From d482d214d9b32e7c36bb6c4ce78487481972c77b Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Wed, 27 Mar 2019 09:49:29 +0100 Subject: [PATCH] Line endings fixed so that we don't have CRLF in Git. Better now than later. --- TODO.md | 42 +- UPGRADE.md | 42 +- client/.gitignore | 2 +- client/src/account/styles.scss | 16 +- client/src/campaigns/helpers.js | 68 +- client/src/campaigns/triggers/helpers.js | 70 +- client/src/lib/axios.js | 62 +- client/src/lib/bootstrap-components.js | 662 ++-- client/src/lib/decorator-helpers.js | 256 +- client/src/lib/error-handling.js | 152 +- client/src/lib/helpers.js | 14 +- client/src/lib/i18n.js | 150 +- client/src/lib/namespace.js | 64 +- client/src/lib/page-common.js | 292 +- client/src/lib/permissions.js | 22 +- client/src/lib/public-path.js | 10 +- client/src/lib/sandbox-common.scss | 180 +- client/src/lib/sandboxed-ckeditor.scss | 12 +- client/src/lib/sandboxed-codeeditor.scss | 68 +- client/src/lib/sandboxed-grapesjs.scss | 36 +- client/src/lib/sandboxed-mosaico.scss | 16 +- client/src/lib/tree.scss | 184 +- client/src/lib/urls.js | 118 +- client/src/lists/fields/helpers.js | 110 +- client/src/lists/forms/styles.scss | 22 +- client/src/lists/imports/helpers.js | 92 +- client/src/lists/segments/CUD.scss | 304 +- client/src/lists/segments/helpers.js | 918 ++--- client/src/lists/styles.scss | 12 +- client/src/lists/subscriptions/helpers.js | 422 +-- client/src/send-configurations/helpers.js | 674 ++-- client/src/templates/helpers.js | 1238 +++---- client/src/templates/mosaico/helpers.js | 100 +- .../static/fancytree/skin-bootstrap/README.md | 2 +- client/static/mosaico/uploads/.gitignore | 4 +- .../mosaico/vendor/notoregular/stylesheet.css | 14 +- docs/access-control.md | 280 +- locales/extract.js | 726 ++-- mvis/client/.gitignore | 2 +- mvis/server/.gitignore | 4 +- mvis/server/config/default.yaml | 154 +- mvis/test-embed/.gitignore | 4 +- mvis/test-embed/views/panel.hbs | 16 +- server/lib/campaign-content.js | 62 +- server/lib/entity-settings.js | 300 +- server/lib/log.js | 14 +- server/lib/namespace-helpers.js | 46 +- server/lib/nodeify.js | 30 +- server/lib/urls.js | 142 +- server/protected/reports/.gitignore | 4 +- server/setup/knex/config.js | 18 +- server/setup/knex/knexfile.js | 16 +- server/views/partials/tracking-scripts.hbs | 36 +- shared/campaigns.js | 154 +- shared/date.js | 148 +- shared/imports.js | 162 +- shared/interoperable-errors.js | 298 +- shared/langs.js | 130 +- shared/lists.js | 100 +- shared/mosaico-templates.js | 3080 ++++++++--------- shared/namespaces.js | 16 +- shared/password-validator.js | 72 +- shared/reports.js | 30 +- shared/send-configurations.js | 56 +- shared/templates.js | 122 +- shared/triggers.js | 94 +- shared/urls.js | 12 +- shared/users.js | 16 +- shared/validators.js | 16 +- 69 files changed, 6405 insertions(+), 6405 deletions(-) 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 ( - - ); - } -} - -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.children} -
- -
- ); - } -} - -@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.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 ( + + ); + } +} + +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.children} +
+ +
+ ); + } +} + +@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.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 ?