Merge branch 'development' into development-tb
This commit is contained in:
commit
63cfb22025
93 changed files with 8555 additions and 7430 deletions
2
client/.gitignore
vendored
2
client/.gitignore
vendored
|
@ -1 +1 @@
|
|||
/dist
|
||||
/dist
|
||||
|
|
26
client/package-lock.json
generated
26
client/package-lock.json
generated
|
@ -4470,7 +4470,8 @@
|
|||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -4488,11 +4489,13 @@
|
|||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -4505,15 +4508,18 @@
|
|||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -4616,7 +4622,8 @@
|
|||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -4626,6 +4633,7 @@
|
|||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -4638,6 +4646,7 @@
|
|||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
@ -4743,7 +4752,8 @@
|
|||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -4858,6 +4868,7 @@
|
|||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -4875,6 +4886,7 @@
|
|||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -608,13 +608,21 @@ export default class CUD extends Component {
|
|||
sendSettings = [];
|
||||
|
||||
const addOverridable = (id, label) => {
|
||||
sendSettings.push(<CheckBox key={id + '_overriden'} id={id + '_overriden'} label={label} text={t('override')}/>);
|
||||
|
||||
if (this.getFormValue(id + '_overriden')) {
|
||||
sendSettings.push(<InputField key={id + '_override'} id={id + '_override'}/>);
|
||||
} else {
|
||||
if(this.state.sendConfiguration[id + '_overridable']){
|
||||
if (this.getFormValue(id + '_overriden')) {
|
||||
sendSettings.push(<InputField label={t(label)} key={id + '_override'} id={id + '_override'}/>);
|
||||
} else {
|
||||
sendSettings.push(
|
||||
<StaticField key={id + '_original'} label={t(label)} id={id + '_original'} className={styles.formDisabled}>
|
||||
{this.state.sendConfiguration[id]}
|
||||
</StaticField>
|
||||
);
|
||||
}
|
||||
sendSettings.push(<CheckBox key={id + '_overriden'} id={id + '_overriden'} text={t('override')} className={campaignsStyles.overrideCheckbox}/>);
|
||||
}
|
||||
else{
|
||||
sendSettings.push(
|
||||
<StaticField key={id + '_original'} id={id + '_original'} className={styles.formDisabled}>
|
||||
<StaticField key={id + '_original'} label={t(label)} id={id + '_original'} className={styles.formDisabled}>
|
||||
{this.state.sendConfiguration[id]}
|
||||
</StaticField>
|
||||
);
|
||||
|
@ -733,20 +741,32 @@ export default class CUD extends Component {
|
|||
|
||||
<hr/>
|
||||
|
||||
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
|
||||
<Fieldset label={t('sendSettings')}>
|
||||
|
||||
{sendSettings}
|
||||
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
|
||||
|
||||
<InputField id="unsubscribe_url" label={t('customUnsubscribeUrl')}/>
|
||||
{sendSettings}
|
||||
|
||||
<InputField id="unsubscribe_url" label={t('customUnsubscribeUrl')}/>
|
||||
</Fieldset>
|
||||
|
||||
<hr/>
|
||||
|
||||
<CheckBox id="open_tracking_disabled" text={t('disableOpenedTracking')}/>
|
||||
<CheckBox id="click_tracking_disabled" text={t('disableClickedTracking')}/>
|
||||
<Fieldset label={t('Tracking')}>
|
||||
<CheckBox id="open_tracking_disabled" text={t('disableOpenedTracking')}/>
|
||||
<CheckBox id="click_tracking_disabled" text={t('disableClickedTracking')}/>
|
||||
</Fieldset>
|
||||
|
||||
{sourceEdit &&
|
||||
<>
|
||||
<hr/>
|
||||
<Fieldset label={t('template')}>
|
||||
{sourceEdit}
|
||||
</Fieldset>
|
||||
</>
|
||||
}
|
||||
|
||||
{sourceEdit && <hr/> }
|
||||
|
||||
{sourceEdit}
|
||||
|
||||
{templateEdit}
|
||||
|
||||
|
|
|
@ -225,6 +225,18 @@ class SendControls extends Component {
|
|||
await this.refreshEntity();
|
||||
}
|
||||
|
||||
async confirmStart() {
|
||||
const t = this.props.t;
|
||||
this.actionDialog(
|
||||
t('confirmLaunch'),
|
||||
t('doYouWantToLaunchTheCampaign?All'),
|
||||
async () => {
|
||||
await this.startAsync();
|
||||
await this.refreshEntity();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async resetAsync() {
|
||||
const t = this.props.t;
|
||||
this.actionDialog(
|
||||
|
@ -306,7 +318,7 @@ class SendControls extends Component {
|
|||
{this.getFormValue('sendLater') ?
|
||||
<Button className="btn-primary" icon="send" label={(entity.scheduled ? t('rescheduleSend') : t('scheduleSend')) + subscrInfo} onClickAsync={::this.scheduleAsync}/>
|
||||
:
|
||||
<Button className="btn-primary" icon="send" label={t('send') + subscrInfo} onClickAsync={::this.startAsync}/>
|
||||
<Button className="btn-primary" icon="send" label={t('send') + subscrInfo} onClickAsync={::this.confirmStart}/>
|
||||
}
|
||||
{entity.status === CampaignStatus.PAUSED && <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>}
|
||||
</ButtonRow>
|
||||
|
@ -335,7 +347,7 @@ class SendControls extends Component {
|
|||
{t('allMessagesSent!HitContinueIfYouYouWant')}
|
||||
</AlignedRow>
|
||||
<ButtonRow>
|
||||
<Button className="btn-primary" icon="play" label={t('continue') + subscrInfo} onClickAsync={::this.startAsync}/>
|
||||
<Button className="btn-primary" icon="play" label={t('continue') + subscrInfo} onClickAsync={::this.confirmStart}/>
|
||||
<Button className="btn-primary" icon="refresh" label={t('reset')} onClickAsync={::this.resetAsync}/>
|
||||
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
|
||||
</ButtonRow>
|
||||
|
@ -443,11 +455,17 @@ export default class Status extends Component {
|
|||
let sendSettings;
|
||||
if (this.state.sendConfiguration) {
|
||||
sendSettings = [];
|
||||
|
||||
|
||||
const addOverridable = (id, label) => {
|
||||
sendSettings.push(<AlignedRow key={id} label={label}>{entity[id + '_override'] === null ? this.state.sendConfiguration[id] : entity[id + '_override']}</AlignedRow>);
|
||||
if(this.state.sendConfiguration[id + '_overridable'] == 1 && entity[id + '_override'] != null){
|
||||
sendSettings.push(<AlignedRow key={id} label={label}>{entity[id + '_override']}</AlignedRow>);
|
||||
}
|
||||
else{
|
||||
sendSettings.push(<AlignedRow key={id} label={label}>{this.state.sendConfiguration[id]}</AlignedRow>);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
addOverridable('from_name', t('fromName'));
|
||||
addOverridable('from_email', t('fromEmailAddress'));
|
||||
addOverridable('reply_to', t('replytoEmailAddress'));
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
'use strict';
|
||||
|
||||
import {
|
||||
CampaignStatus,
|
||||
CampaignType
|
||||
} from "../../../shared/campaigns";
|
||||
|
||||
export function getCampaignLabels(t) {
|
||||
|
||||
const campaignTypeLabels = {
|
||||
[CampaignType.REGULAR]: t('regular'),
|
||||
[CampaignType.TRIGGERED]: t('triggered'),
|
||||
[CampaignType.RSS]: t('rss')
|
||||
};
|
||||
|
||||
const campaignStatusLabels = {
|
||||
[CampaignStatus.IDLE]: t('idle'),
|
||||
[CampaignStatus.SCHEDULED]: t('scheduled'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.FINISHED]: t('finished'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.INACTIVE]: t('inactive'),
|
||||
[CampaignStatus.ACTIVE]: t('active'),
|
||||
[CampaignStatus.SENDING]: t('sending')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
campaignStatusLabels,
|
||||
campaignTypeLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
CampaignStatus,
|
||||
CampaignType
|
||||
} from "../../../shared/campaigns";
|
||||
|
||||
export function getCampaignLabels(t) {
|
||||
|
||||
const campaignTypeLabels = {
|
||||
[CampaignType.REGULAR]: t('regular'),
|
||||
[CampaignType.TRIGGERED]: t('triggered'),
|
||||
[CampaignType.RSS]: t('rss')
|
||||
};
|
||||
|
||||
const campaignStatusLabels = {
|
||||
[CampaignStatus.IDLE]: t('idle'),
|
||||
[CampaignStatus.SCHEDULED]: t('scheduled'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.FINISHED]: t('finished'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.INACTIVE]: t('inactive'),
|
||||
[CampaignStatus.ACTIVE]: t('active'),
|
||||
[CampaignStatus.SENDING]: t('sending')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
campaignStatusLabels,
|
||||
campaignTypeLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,92 +1,94 @@
|
|||
.entry {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-bottom: 15px;
|
||||
min-height: 91px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0px none;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.entryButtons {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 19px;
|
||||
|
||||
button {
|
||||
padding: 2px 3px;
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.entryWithButtons > .entryContent {
|
||||
margin-right: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.newEntry {
|
||||
text-align: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sendButtonRow {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.statsMetrics {
|
||||
width: 10ex;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.statsProgressBar {
|
||||
margin-right: 30px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.statsProgressBarZoomIn {
|
||||
float: right;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zoomIn {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.navPills {
|
||||
margin-top: -3px;
|
||||
margin-bottom: 5px;
|
||||
float: right;
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
|
||||
& > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
.entry {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-bottom: 15px;
|
||||
min-height: 91px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0px none;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.entryButtons {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 19px;
|
||||
|
||||
button {
|
||||
padding: 2px 3px;
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.entryWithButtons > .entryContent {
|
||||
margin-right: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.newEntry {
|
||||
text-align: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sendButtonRow {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.statsMetrics {
|
||||
width: 10ex;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.statsProgressBar {
|
||||
margin-right: 30px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.statsProgressBarZoomIn {
|
||||
float: right;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zoomIn {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.navPills {
|
||||
margin-top: -3px;
|
||||
margin-bottom: 5px;
|
||||
float: right;
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
|
||||
& > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.overrideCheckbox{
|
||||
margin-top: -8px !important;
|
||||
}
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
import {Entity, Event} from '../../../../shared/triggers';
|
||||
|
||||
export function getTriggerTypes(t) {
|
||||
|
||||
const entityLabels = {
|
||||
[Entity.SUBSCRIPTION]: t('subscription'),
|
||||
[Entity.CAMPAIGN]: t('campaign')
|
||||
};
|
||||
|
||||
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
|
||||
const CampaignEvent = Event[Entity.CAMPAIGN];
|
||||
|
||||
const eventLabels = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
[SubscriptionEvent.CREATED]: t('created'),
|
||||
[SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
|
||||
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
[CampaignEvent.DELIVERED]: t('delivered'),
|
||||
[CampaignEvent.OPENED]: t('opened'),
|
||||
[CampaignEvent.CLICKED]: t('clicked'),
|
||||
[CampaignEvent.NOT_OPENED]: t('notOpened'),
|
||||
[CampaignEvent.NOT_CLICKED]: t('notClicked')
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
entityLabels,
|
||||
eventLabels
|
||||
};
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import {Entity, Event} from '../../../../shared/triggers';
|
||||
|
||||
export function getTriggerTypes(t) {
|
||||
|
||||
const entityLabels = {
|
||||
[Entity.SUBSCRIPTION]: t('subscription'),
|
||||
[Entity.CAMPAIGN]: t('campaign')
|
||||
};
|
||||
|
||||
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
|
||||
const CampaignEvent = Event[Entity.CAMPAIGN];
|
||||
|
||||
const eventLabels = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
[SubscriptionEvent.CREATED]: t('created'),
|
||||
[SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
|
||||
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
[CampaignEvent.DELIVERED]: t('delivered'),
|
||||
[CampaignEvent.OPENED]: t('opened'),
|
||||
[CampaignEvent.CLICKED]: t('clicked'),
|
||||
[CampaignEvent.NOT_OPENED]: t('notOpened'),
|
||||
[CampaignEvent.NOT_CLICKED]: t('notClicked')
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
entityLabels,
|
||||
eventLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
import csrfToken from 'csrfToken';
|
||||
import axios from 'axios';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
const axiosInst = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const axiosWrapper = {
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
|
||||
};
|
||||
|
||||
const HTTPMethod = {
|
||||
GET: axiosWrapper.get,
|
||||
PUT: axiosWrapper.put,
|
||||
POST: axiosWrapper.post,
|
||||
DELETE: axiosWrapper.delete
|
||||
};
|
||||
|
||||
axiosWrapper.method = (method, ...args) => method(...args);
|
||||
|
||||
export default axiosWrapper;
|
||||
export {
|
||||
HTTPMethod
|
||||
'use strict';
|
||||
|
||||
import csrfToken from 'csrfToken';
|
||||
import axios from 'axios';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
const axiosInst = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const axiosWrapper = {
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
|
||||
};
|
||||
|
||||
const HTTPMethod = {
|
||||
GET: axiosWrapper.get,
|
||||
PUT: axiosWrapper.put,
|
||||
POST: axiosWrapper.post,
|
||||
DELETE: axiosWrapper.delete
|
||||
};
|
||||
|
||||
axiosWrapper.method = (method, ...args) => method(...args);
|
||||
|
||||
export default axiosWrapper;
|
||||
export {
|
||||
HTTPMethod
|
||||
}
|
662
client/src/lib/bootstrap-components.js
vendored
662
client/src/lib/bootstrap-components.js
vendored
|
@ -1,331 +1,331 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from './error-handling';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class DismissibleAlert extends Component {
|
||||
static propTypes = {
|
||||
severity: PropTypes.string.isRequired,
|
||||
onCloseAsync: PropTypes.func
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Icon extends Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
family: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
family: 'fas'
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
if (props.family === 'fas' || props.family === 'far') {
|
||||
return <i className={`${props.family} fa-${props.icon} ${props.className || ''}`} title={props.title}></i>;
|
||||
} else {
|
||||
console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class Button extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconTitle: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'btn';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
let type = props.type || 'button';
|
||||
|
||||
let icon;
|
||||
if (props.icon) {
|
||||
icon = <Icon icon={props.icon} title={props.iconTitle}/>
|
||||
}
|
||||
|
||||
let iconSpacer;
|
||||
if (props.icon && props.label) {
|
||||
iconSpacer = ' ';
|
||||
}
|
||||
|
||||
return (
|
||||
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonDropdown extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
className: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
|
||||
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
|
||||
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
|
||||
|
||||
return (
|
||||
<div className="dropdown" className={className}>
|
||||
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{props.label}
|
||||
</button>
|
||||
<ul className={menuClassName}>
|
||||
{props.children}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class ActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
href: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return (
|
||||
<a href={props.href || ''} className={props.className} onClick={::this.onClick}>{props.children}</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let clsName = "dropdown-item ";
|
||||
if (props.disabled) {
|
||||
clsName += "disabled ";
|
||||
}
|
||||
|
||||
clsName += props.className;
|
||||
|
||||
return (
|
||||
<ActionLink className={clsName} onClickAsync={props.onClickAsync}>{props.children}</ActionLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownDivider extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'dropdown-divider';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class ModalDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ]
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCloseAsync: PropTypes.func,
|
||||
onButtonClickAsync: PropTypes.func,
|
||||
buttons: PropTypes.array,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
/*
|
||||
this.props.hidden - this is the desired state of the modal
|
||||
this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
|
||||
jqModal.on('shown.bs.modal', () => jqModal.focus());
|
||||
jqModal.on('hide.bs.modal', ::this.onHide);
|
||||
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal({
|
||||
show: !this.props.hidden
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.hidden != this.hidden) {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal(this.props.hidden ? 'hide' : 'show');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too.
|
||||
jQuery('.modal-backdrop').remove();
|
||||
}
|
||||
|
||||
onHide(evt) {
|
||||
// Hide event is emited is both when hidden through user action or through API. We have to let the API
|
||||
// calls through, otherwise the modal would never hide. The user actions, which change the desired state,
|
||||
// are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to
|
||||
// hide the modal or not.
|
||||
if (!this.props.hidden) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onClose();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
await this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
async onButtonClick(idx) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
if (buttonSpec.onClickAsync) {
|
||||
await buttonSpec.onClickAsync(idx);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const t = props.t;
|
||||
|
||||
const buttons = [];
|
||||
for (let idx = 0; idx < this.state.buttons.length; idx++) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
|
||||
buttons.push(button);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(domElem) => { this.domModal = domElem; }}
|
||||
className={'modal fade' + (props.className ? ' ' + props.className : '')}
|
||||
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">{this.props.title}</h4>
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div className="modal-body">{this.props.children}</div>
|
||||
<div className="modal-footer">
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from './error-handling';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class DismissibleAlert extends Component {
|
||||
static propTypes = {
|
||||
severity: PropTypes.string.isRequired,
|
||||
onCloseAsync: PropTypes.func
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Icon extends Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
family: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
family: 'fas'
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
if (props.family === 'fas' || props.family === 'far') {
|
||||
return <i className={`${props.family} fa-${props.icon} ${props.className || ''}`} title={props.title}></i>;
|
||||
} else {
|
||||
console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class Button extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconTitle: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'btn';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
let type = props.type || 'button';
|
||||
|
||||
let icon;
|
||||
if (props.icon) {
|
||||
icon = <Icon icon={props.icon} title={props.iconTitle}/>
|
||||
}
|
||||
|
||||
let iconSpacer;
|
||||
if (props.icon && props.label) {
|
||||
iconSpacer = ' ';
|
||||
}
|
||||
|
||||
return (
|
||||
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonDropdown extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
className: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
|
||||
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
|
||||
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
|
||||
|
||||
return (
|
||||
<div className="dropdown" className={className}>
|
||||
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{props.label}
|
||||
</button>
|
||||
<ul className={menuClassName}>
|
||||
{props.children}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class ActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
href: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return (
|
||||
<a href={props.href || ''} className={props.className} onClick={::this.onClick}>{props.children}</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let clsName = "dropdown-item ";
|
||||
if (props.disabled) {
|
||||
clsName += "disabled ";
|
||||
}
|
||||
|
||||
clsName += props.className;
|
||||
|
||||
return (
|
||||
<ActionLink className={clsName} onClickAsync={props.onClickAsync}>{props.children}</ActionLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownDivider extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'dropdown-divider';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class ModalDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ]
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCloseAsync: PropTypes.func,
|
||||
onButtonClickAsync: PropTypes.func,
|
||||
buttons: PropTypes.array,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
/*
|
||||
this.props.hidden - this is the desired state of the modal
|
||||
this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
|
||||
jqModal.on('shown.bs.modal', () => jqModal.focus());
|
||||
jqModal.on('hide.bs.modal', ::this.onHide);
|
||||
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal({
|
||||
show: !this.props.hidden
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.hidden != this.hidden) {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal(this.props.hidden ? 'hide' : 'show');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too.
|
||||
jQuery('.modal-backdrop').remove();
|
||||
}
|
||||
|
||||
onHide(evt) {
|
||||
// Hide event is emited is both when hidden through user action or through API. We have to let the API
|
||||
// calls through, otherwise the modal would never hide. The user actions, which change the desired state,
|
||||
// are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to
|
||||
// hide the modal or not.
|
||||
if (!this.props.hidden) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onClose();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
await this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
async onButtonClick(idx) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
if (buttonSpec.onClickAsync) {
|
||||
await buttonSpec.onClickAsync(idx);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const t = props.t;
|
||||
|
||||
const buttons = [];
|
||||
for (let idx = 0; idx < this.state.buttons.length; idx++) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
|
||||
buttons.push(button);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(domElem) => { this.domModal = domElem; }}
|
||||
className={'modal fade' + (props.className ? ' ' + props.className : '')}
|
||||
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">{this.props.title}</h4>
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div className="modal-body">{this.props.children}</div>
|
||||
<div className="modal-footer">
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,128 +1,128 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function createComponentMixin(contexts, deps, decoratorFn) {
|
||||
return {
|
||||
contexts,
|
||||
deps,
|
||||
decoratorFn
|
||||
};
|
||||
}
|
||||
|
||||
export function withComponentMixins(mixins, delegateFuns) {
|
||||
const mixinsClosure = new Set();
|
||||
for (const mixin of mixins) {
|
||||
mixinsClosure.add(mixin);
|
||||
for (const dep of mixin.deps) {
|
||||
mixinsClosure.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
const contexts = new Map();
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
for (const ctx of mixin.contexts) {
|
||||
contexts.set(ctx.propName, ctx.context);
|
||||
}
|
||||
}
|
||||
|
||||
return TargetClass => {
|
||||
const ctors = [];
|
||||
const mixinDelegateFuns = [];
|
||||
|
||||
if (delegateFuns) {
|
||||
mixinDelegateFuns.push(...delegateFuns);
|
||||
}
|
||||
|
||||
function TargetClassWithCtors(props) {
|
||||
if (!new.target) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const self = Reflect.construct(TargetClass, [props], new.target);
|
||||
|
||||
for (const ctor of ctors) {
|
||||
ctor(self, props);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
TargetClassWithCtors.prototype = TargetClass.prototype;
|
||||
|
||||
for (const attr in TargetClass) {
|
||||
TargetClassWithCtors[attr] = TargetClass[attr];
|
||||
}
|
||||
|
||||
|
||||
class ComponentMixinsInner extends React.Component {
|
||||
render() {
|
||||
const props = {
|
||||
...this.props,
|
||||
ref: this.props._decoratorInnerInstanceRefFn
|
||||
};
|
||||
delete props._decoratorInnerInstanceRefFn;
|
||||
|
||||
return (
|
||||
<TargetClassWithCtors {...props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let DecoratedInner = ComponentMixinsInner;
|
||||
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors);
|
||||
|
||||
if (res.cls) {
|
||||
DecoratedInner = res.cls;
|
||||
}
|
||||
|
||||
if (res.ctor) {
|
||||
ctors.push(res.ctor);
|
||||
}
|
||||
|
||||
if (res.delegateFuns) {
|
||||
mixinDelegateFuns.push(...res.delegateFuns);
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentMixinsOuter extends React.Component {
|
||||
render() {
|
||||
let innerFn = parentProps => {
|
||||
const props = {
|
||||
...parentProps,
|
||||
_decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node
|
||||
};
|
||||
|
||||
return <DecoratedInner {...props}/>
|
||||
}
|
||||
|
||||
for (const [propName, Context] of contexts.entries()) {
|
||||
const existingInnerFn = innerFn;
|
||||
innerFn = parentProps => (
|
||||
<Context.Consumer>
|
||||
{
|
||||
value => existingInnerFn({
|
||||
...parentProps,
|
||||
[propName]: value
|
||||
})
|
||||
}
|
||||
</Context.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
return innerFn(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
for (const fun of mixinDelegateFuns) {
|
||||
ComponentMixinsOuter.prototype[fun] = function (...args) {
|
||||
return this._decoratorInnerInstance[fun](...args);
|
||||
}
|
||||
}
|
||||
|
||||
return ComponentMixinsOuter;
|
||||
};
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function createComponentMixin(contexts, deps, decoratorFn) {
|
||||
return {
|
||||
contexts,
|
||||
deps,
|
||||
decoratorFn
|
||||
};
|
||||
}
|
||||
|
||||
export function withComponentMixins(mixins, delegateFuns) {
|
||||
const mixinsClosure = new Set();
|
||||
for (const mixin of mixins) {
|
||||
mixinsClosure.add(mixin);
|
||||
for (const dep of mixin.deps) {
|
||||
mixinsClosure.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
const contexts = new Map();
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
for (const ctx of mixin.contexts) {
|
||||
contexts.set(ctx.propName, ctx.context);
|
||||
}
|
||||
}
|
||||
|
||||
return TargetClass => {
|
||||
const ctors = [];
|
||||
const mixinDelegateFuns = [];
|
||||
|
||||
if (delegateFuns) {
|
||||
mixinDelegateFuns.push(...delegateFuns);
|
||||
}
|
||||
|
||||
function TargetClassWithCtors(props) {
|
||||
if (!new.target) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const self = Reflect.construct(TargetClass, [props], new.target);
|
||||
|
||||
for (const ctor of ctors) {
|
||||
ctor(self, props);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
TargetClassWithCtors.prototype = TargetClass.prototype;
|
||||
|
||||
for (const attr in TargetClass) {
|
||||
TargetClassWithCtors[attr] = TargetClass[attr];
|
||||
}
|
||||
|
||||
|
||||
class ComponentMixinsInner extends React.Component {
|
||||
render() {
|
||||
const props = {
|
||||
...this.props,
|
||||
ref: this.props._decoratorInnerInstanceRefFn
|
||||
};
|
||||
delete props._decoratorInnerInstanceRefFn;
|
||||
|
||||
return (
|
||||
<TargetClassWithCtors {...props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let DecoratedInner = ComponentMixinsInner;
|
||||
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors);
|
||||
|
||||
if (res.cls) {
|
||||
DecoratedInner = res.cls;
|
||||
}
|
||||
|
||||
if (res.ctor) {
|
||||
ctors.push(res.ctor);
|
||||
}
|
||||
|
||||
if (res.delegateFuns) {
|
||||
mixinDelegateFuns.push(...res.delegateFuns);
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentMixinsOuter extends React.Component {
|
||||
render() {
|
||||
let innerFn = parentProps => {
|
||||
const props = {
|
||||
...parentProps,
|
||||
_decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node
|
||||
};
|
||||
|
||||
return <DecoratedInner {...props}/>
|
||||
}
|
||||
|
||||
for (const [propName, Context] of contexts.entries()) {
|
||||
const existingInnerFn = innerFn;
|
||||
innerFn = parentProps => (
|
||||
<Context.Consumer>
|
||||
{
|
||||
value => existingInnerFn({
|
||||
...parentProps,
|
||||
[propName]: value
|
||||
})
|
||||
}
|
||||
</Context.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
return innerFn(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
for (const fun of mixinDelegateFuns) {
|
||||
ComponentMixinsOuter.prototype[fun] = function (...args) {
|
||||
return this._decoratorInnerInstance[fun](...args);
|
||||
}
|
||||
}
|
||||
|
||||
return ComponentMixinsOuter;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +1,76 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
function handleError(that, error) {
|
||||
let errorHandled;
|
||||
if (that.errorHandler) {
|
||||
errorHandled = that.errorHandler(error);
|
||||
}
|
||||
|
||||
if (!errorHandled && that.props.parentErrorHandler) {
|
||||
errorHandled = handleError(that.props.parentErrorHandler, error);
|
||||
}
|
||||
|
||||
if (!errorHandled) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return errorHandled;
|
||||
}
|
||||
|
||||
export const ParentErrorHandlerContext = React.createContext(null);
|
||||
export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => {
|
||||
/* Example of use:
|
||||
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
|
||||
|
||||
It's equivalent to:
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(...);
|
||||
}
|
||||
*/
|
||||
|
||||
const originalRender = InnerClass.prototype.render;
|
||||
|
||||
InnerClass.prototype.render = function() {
|
||||
return (
|
||||
<ParentErrorHandlerContext.Provider value={this}>
|
||||
{originalRender.apply(this)}
|
||||
</ParentErrorHandlerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
InnerClass.prototype.handleError = function(error) {
|
||||
handleError(this, error);
|
||||
};
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
export function withAsyncErrorHandler(target, name, descriptor) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
descriptor.value = async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(this, error);
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
export function wrapWithAsyncErrorHandler(self, fn) {
|
||||
return async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(self, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
function handleError(that, error) {
|
||||
let errorHandled;
|
||||
if (that.errorHandler) {
|
||||
errorHandled = that.errorHandler(error);
|
||||
}
|
||||
|
||||
if (!errorHandled && that.props.parentErrorHandler) {
|
||||
errorHandled = handleError(that.props.parentErrorHandler, error);
|
||||
}
|
||||
|
||||
if (!errorHandled) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return errorHandled;
|
||||
}
|
||||
|
||||
export const ParentErrorHandlerContext = React.createContext(null);
|
||||
export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => {
|
||||
/* Example of use:
|
||||
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
|
||||
|
||||
It's equivalent to:
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(...);
|
||||
}
|
||||
*/
|
||||
|
||||
const originalRender = InnerClass.prototype.render;
|
||||
|
||||
InnerClass.prototype.render = function() {
|
||||
return (
|
||||
<ParentErrorHandlerContext.Provider value={this}>
|
||||
{originalRender.apply(this)}
|
||||
</ParentErrorHandlerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
InnerClass.prototype.handleError = function(error) {
|
||||
handleError(this, error);
|
||||
};
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
export function withAsyncErrorHandler(target, name, descriptor) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
descriptor.value = async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(this, error);
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
export function wrapWithAsyncErrorHandler(self, fn) {
|
||||
return async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(self, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -339,7 +339,8 @@ class CheckBox extends Component {
|
|||
text: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
format: PropTypes.string
|
||||
format: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -348,12 +349,12 @@ class CheckBox extends Component {
|
|||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
const className = owner.addFormValidationClass('form-check-input', id);
|
||||
const inputClassName = owner.addFormValidationClass('form-check-input', id);
|
||||
|
||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||
<div className="form-group form-check my-2">
|
||||
<input className={className} type="checkbox" checked={owner.getFormValue(id)} id={htmlId} aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, !owner.getFormValue(id))}/>
|
||||
<label className="form-check-label" htmlFor={htmlId}>{props.text}</label>
|
||||
<div className={`form-group form-check my-2 ${this.props.className}`}>
|
||||
<input className={inputClassName} type="checkbox" checked={owner.getFormValue(id)} id={htmlId} aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, !owner.getFormValue(id))}/>
|
||||
<label className={styles.checkboxText} htmlFor={htmlId}>{props.text}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
import ellipsize from "ellipsize";
|
||||
|
||||
|
||||
export function ellipsizeBreadcrumbLabel(label) {
|
||||
return ellipsize(label, 40)
|
||||
'use strict';
|
||||
|
||||
import ellipsize from "ellipsize";
|
||||
|
||||
|
||||
export function ellipsizeBreadcrumbLabel(label) {
|
||||
return ellipsize(label, 40)
|
||||
}
|
|
@ -1,72 +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";
|
||||
|
||||
const resourcesCommon = {
|
||||
'en-US': lang_en_US_common,
|
||||
'fk-FK': convertToFake(lang_en_US_common)
|
||||
};
|
||||
|
||||
const resources = {};
|
||||
for (const lng of mailtrainConfig.enabledLanguages) {
|
||||
const langDesc = getLang(lng);
|
||||
resources[langDesc.longCode] = {
|
||||
common: resourcesCommon[langDesc.longCode]
|
||||
};
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
resources,
|
||||
|
||||
fallbackLng: mailtrainConfig.defaultLanguage,
|
||||
defaultNS: 'common',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react
|
||||
},
|
||||
|
||||
react: {
|
||||
wait: true
|
||||
},
|
||||
|
||||
detection: {
|
||||
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
|
||||
lookupQuerystring: 'locale',
|
||||
lookupCookie: 'i18nextLng',
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
caches: ['localStorage', 'cookie']
|
||||
},
|
||||
|
||||
whitelist: mailtrainConfig.enabledLanguages,
|
||||
load: 'currentOnly',
|
||||
|
||||
debug: false
|
||||
});
|
||||
|
||||
|
||||
export default i18n;
|
||||
|
||||
|
||||
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
|
||||
return {
|
||||
cls: withNamespaces()(TargetClass)
|
||||
};
|
||||
});
|
||||
|
||||
export function tMark(key) {
|
||||
return key;
|
||||
}
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import i18n
|
||||
from 'i18next';
|
||||
import {withNamespaces} from "react-i18next";
|
||||
import LanguageDetector
|
||||
from 'i18next-browser-languagedetector';
|
||||
import mailtrainConfig
|
||||
from 'mailtrainConfig';
|
||||
|
||||
import {convertToFake, getLang} from '../../../shared/langs';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
import lang_en_US_common from "../../../locales/en-US/common";
|
||||
import lang_es_ES_common from "../../../locales/es-ES/common";
|
||||
|
||||
|
||||
const resourcesCommon = {
|
||||
'en-US': lang_en_US_common,
|
||||
'es-ES': lang_es_ES_common,
|
||||
'fk-FK': convertToFake(lang_en_US_common)
|
||||
};
|
||||
|
||||
const resources = {};
|
||||
for (const lng of mailtrainConfig.enabledLanguages) {
|
||||
const langDesc = getLang(lng);
|
||||
resources[langDesc.longCode] = {
|
||||
common: resourcesCommon[langDesc.longCode]
|
||||
};
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
resources,
|
||||
|
||||
fallbackLng: mailtrainConfig.defaultLanguage,
|
||||
defaultNS: 'common',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react
|
||||
},
|
||||
|
||||
react: {
|
||||
wait: true
|
||||
},
|
||||
|
||||
detection: {
|
||||
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
|
||||
lookupQuerystring: 'locale',
|
||||
lookupCookie: 'i18nextLng',
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
caches: ['localStorage', 'cookie']
|
||||
},
|
||||
|
||||
whitelist: mailtrainConfig.enabledLanguages,
|
||||
load: 'currentOnly',
|
||||
|
||||
debug: false
|
||||
});
|
||||
|
||||
|
||||
export default i18n;
|
||||
|
||||
|
||||
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
|
||||
return {
|
||||
cls: withNamespaces()(TargetClass)
|
||||
};
|
||||
});
|
||||
|
||||
export function tMark(key) {
|
||||
return key;
|
||||
}
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import {TreeTableSelect} from './form';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation
|
||||
])
|
||||
class NamespaceSelect extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<TreeTableSelect id="namespace" label={t('namespace')} dataUrl="rest/namespaces-tree"/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNamespace(t, state) {
|
||||
if (!state.getIn(['namespace', 'value'])) {
|
||||
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
|
||||
} else {
|
||||
state.setIn(['namespace', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
NamespaceSelect,
|
||||
validateNamespace
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import {TreeTableSelect} from './form';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation
|
||||
])
|
||||
class NamespaceSelect extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<TreeTableSelect id="namespace" label={t('namespace')} dataUrl="rest/namespaces-tree"/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNamespace(t, state) {
|
||||
if (!state.getIn(['namespace', 'value'])) {
|
||||
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
|
||||
} else {
|
||||
state.setIn(['namespace', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
NamespaceSelect,
|
||||
validateNamespace
|
||||
};
|
|
@ -1,146 +1,146 @@
|
|||
'use strict';
|
||||
|
||||
import React
|
||||
from "react";
|
||||
import {withRouter} from "react-router";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import axios
|
||||
from "../lib/axios";
|
||||
import {getUrl} from "./urls";
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
export function needsResolve(route, nextRoute, match, nextMatch) {
|
||||
const resolve = route.resolve;
|
||||
const nextResolve = nextRoute.resolve;
|
||||
|
||||
// This compares whether two objects have the same content and returns TRUE if they don't
|
||||
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
|
||||
for (const key in nextResolve) {
|
||||
if (!(key in resolve) ||
|
||||
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolve(route, match) {
|
||||
const keys = Object.keys(route.resolve);
|
||||
|
||||
const promises = keys.map(key => {
|
||||
const url = route.resolve[key](match.params);
|
||||
if (url) {
|
||||
return axios.get(getUrl(url));
|
||||
} else {
|
||||
return Promise.resolve({data: null});
|
||||
}
|
||||
});
|
||||
const resolvedArr = await Promise.all(promises);
|
||||
|
||||
const resolved = {};
|
||||
for (let idx = 0; idx < keys.length; idx++) {
|
||||
resolved[keys[idx]] = resolvedArr[idx].data;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
|
||||
let routes = [];
|
||||
for (let routeKey in structure) {
|
||||
const entry = structure[routeKey];
|
||||
|
||||
let path = urlPrefix + routeKey;
|
||||
let pathWithParams = path;
|
||||
|
||||
if (entry.extraParams) {
|
||||
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
|
||||
}
|
||||
|
||||
let entryResolve;
|
||||
if (entry.resolve) {
|
||||
entryResolve = Object.assign({}, resolve, entry.resolve);
|
||||
} else {
|
||||
entryResolve = resolve;
|
||||
}
|
||||
|
||||
let navKeys;
|
||||
const entryNavs = [];
|
||||
if (entry.navs) {
|
||||
navKeys = Object.keys(entry.navs);
|
||||
|
||||
for (const navKey of navKeys) {
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
entryNavs.push({
|
||||
title: nav.title,
|
||||
visible: nav.visible,
|
||||
link: nav.link,
|
||||
externalLink: nav.externalLink
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const route = {
|
||||
path: (pathWithParams === '' ? '/' : pathWithParams),
|
||||
panelComponent: entry.panelComponent,
|
||||
panelRender: entry.panelRender,
|
||||
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
|
||||
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
|
||||
title: entry.title,
|
||||
link: entry.link,
|
||||
panelInFullScreen: entry.panelInFullScreen,
|
||||
insideIframe: entry.insideIframe,
|
||||
resolve: entryResolve,
|
||||
parents,
|
||||
navs: [...navs, ...entryNavs]
|
||||
};
|
||||
|
||||
routes.push(route);
|
||||
|
||||
const childrenParents = [...parents, route];
|
||||
|
||||
if (entry.navs) {
|
||||
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
|
||||
const navKey = navKeys[navKeyIdx];
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
const childNavs = [...entryNavs];
|
||||
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
|
||||
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.children) {
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export const SectionContentContext = React.createContext(null);
|
||||
export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => {
|
||||
InnerClass.prototype.setFlashMessage = function(severity, text) {
|
||||
return this.props.sectionContent.setFlashMessage(severity, text);
|
||||
};
|
||||
|
||||
InnerClass.prototype.navigateTo = function(path) {
|
||||
return this.props.sectionContent.navigateTo(path);
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateBack = function() {
|
||||
return this.props.sectionContent.navigateBack();
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) {
|
||||
return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
'use strict';
|
||||
|
||||
import React
|
||||
from "react";
|
||||
import {withRouter} from "react-router";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import axios
|
||||
from "../lib/axios";
|
||||
import {getUrl} from "./urls";
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
export function needsResolve(route, nextRoute, match, nextMatch) {
|
||||
const resolve = route.resolve;
|
||||
const nextResolve = nextRoute.resolve;
|
||||
|
||||
// This compares whether two objects have the same content and returns TRUE if they don't
|
||||
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
|
||||
for (const key in nextResolve) {
|
||||
if (!(key in resolve) ||
|
||||
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolve(route, match) {
|
||||
const keys = Object.keys(route.resolve);
|
||||
|
||||
const promises = keys.map(key => {
|
||||
const url = route.resolve[key](match.params);
|
||||
if (url) {
|
||||
return axios.get(getUrl(url));
|
||||
} else {
|
||||
return Promise.resolve({data: null});
|
||||
}
|
||||
});
|
||||
const resolvedArr = await Promise.all(promises);
|
||||
|
||||
const resolved = {};
|
||||
for (let idx = 0; idx < keys.length; idx++) {
|
||||
resolved[keys[idx]] = resolvedArr[idx].data;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
|
||||
let routes = [];
|
||||
for (let routeKey in structure) {
|
||||
const entry = structure[routeKey];
|
||||
|
||||
let path = urlPrefix + routeKey;
|
||||
let pathWithParams = path;
|
||||
|
||||
if (entry.extraParams) {
|
||||
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
|
||||
}
|
||||
|
||||
let entryResolve;
|
||||
if (entry.resolve) {
|
||||
entryResolve = Object.assign({}, resolve, entry.resolve);
|
||||
} else {
|
||||
entryResolve = resolve;
|
||||
}
|
||||
|
||||
let navKeys;
|
||||
const entryNavs = [];
|
||||
if (entry.navs) {
|
||||
navKeys = Object.keys(entry.navs);
|
||||
|
||||
for (const navKey of navKeys) {
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
entryNavs.push({
|
||||
title: nav.title,
|
||||
visible: nav.visible,
|
||||
link: nav.link,
|
||||
externalLink: nav.externalLink
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const route = {
|
||||
path: (pathWithParams === '' ? '/' : pathWithParams),
|
||||
panelComponent: entry.panelComponent,
|
||||
panelRender: entry.panelRender,
|
||||
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
|
||||
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
|
||||
title: entry.title,
|
||||
link: entry.link,
|
||||
panelInFullScreen: entry.panelInFullScreen,
|
||||
insideIframe: entry.insideIframe,
|
||||
resolve: entryResolve,
|
||||
parents,
|
||||
navs: [...navs, ...entryNavs]
|
||||
};
|
||||
|
||||
routes.push(route);
|
||||
|
||||
const childrenParents = [...parents, route];
|
||||
|
||||
if (entry.navs) {
|
||||
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
|
||||
const navKey = navKeys[navKeyIdx];
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
const childNavs = [...entryNavs];
|
||||
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
|
||||
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.children) {
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export const SectionContentContext = React.createContext(null);
|
||||
export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => {
|
||||
InnerClass.prototype.setFlashMessage = function(severity, text) {
|
||||
return this.props.sectionContent.setFlashMessage(severity, text);
|
||||
};
|
||||
|
||||
InnerClass.prototype.navigateTo = function(path) {
|
||||
return this.props.sectionContent.navigateTo(path);
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateBack = function() {
|
||||
return this.props.sectionContent.navigateBack();
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) {
|
||||
return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
import axios from "./axios";
|
||||
|
||||
async function checkPermissions(request) {
|
||||
return await axios.post(getUrl('rest/permissions-check'), request);
|
||||
}
|
||||
|
||||
export {
|
||||
checkPermissions
|
||||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
import axios from "./axios";
|
||||
|
||||
async function checkPermissions(request) {
|
||||
return await axios.post(getUrl('rest/permissions-check'), request);
|
||||
}
|
||||
|
||||
export {
|
||||
checkPermissions
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
__webpack_public_path__ = getUrl('client/');
|
||||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
__webpack_public_path__ = getUrl('client/');
|
||||
|
|
|
@ -1,90 +1,90 @@
|
|||
$navbarHeight: 34px;
|
||||
$editorNormalHeight: 800px !default;
|
||||
|
||||
.editor {
|
||||
.host {
|
||||
@if $editorNormalHeight {
|
||||
height: $editorNormalHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editorFullscreen {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
margin-top: $navbarHeight;
|
||||
|
||||
.navbar {
|
||||
margin-top: -$navbarHeight;
|
||||
}
|
||||
|
||||
.host {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #f86c6b;
|
||||
width: 100%;
|
||||
height: $navbarHeight;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbarLeft {
|
||||
.logo {
|
||||
display: inline-block;
|
||||
height: $navbarHeight;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
padding: 5px 0 5px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
color: white;
|
||||
height: $navbarHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.navbarRight {
|
||||
.btn, .btnDisabled {
|
||||
display: inline-block;
|
||||
padding: 0px 15px;
|
||||
line-height: $navbarHeight;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #c05454;
|
||||
text-decoration: none;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btnDisabled {
|
||||
cursor: default;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: #621d1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
$navbarHeight: 34px;
|
||||
$editorNormalHeight: 800px !default;
|
||||
|
||||
.editor {
|
||||
.host {
|
||||
@if $editorNormalHeight {
|
||||
height: $editorNormalHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editorFullscreen {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
margin-top: $navbarHeight;
|
||||
|
||||
.navbar {
|
||||
margin-top: -$navbarHeight;
|
||||
}
|
||||
|
||||
.host {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #f86c6b;
|
||||
width: 100%;
|
||||
height: $navbarHeight;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbarLeft {
|
||||
.logo {
|
||||
display: inline-block;
|
||||
height: $navbarHeight;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
padding: 5px 0 5px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
color: white;
|
||||
height: $navbarHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.navbarRight {
|
||||
.btn, .btnDisabled {
|
||||
display: inline-block;
|
||||
padding: 0px 15px;
|
||||
line-height: $navbarHeight;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #c05454;
|
||||
text-decoration: none;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btnDisabled {
|
||||
cursor: default;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: #621d1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$editorNormalHeight: false;
|
||||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
$editorNormalHeight: false;
|
||||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,35 +1,35 @@
|
|||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
}
|
||||
|
||||
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aceEditorWithPreview {
|
||||
border-right: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.aceEditorWithoutPreview {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview {
|
||||
border-left: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px none;
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
}
|
||||
|
||||
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aceEditorWithPreview {
|
||||
border-right: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.aceEditorWithoutPreview {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview {
|
||||
border-left: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px none;
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
@import "sandbox-common";
|
||||
|
||||
:global .grapesjs-body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
:global .gjs-editor-cont {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:global .gjs-devices-c .gjs-devices {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
:global .gjs-pn-devices-c, :global .gjs-pn-views {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@import "sandbox-common";
|
||||
|
||||
:global .grapesjs-body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
:global .gjs-editor-cont {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:global .gjs-devices-c .gjs-devices {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
:global .gjs-pn-devices-c, :global .gjs-pn-views {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@import "sandbox-common";
|
||||
|
||||
:global .mo-standalone {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
@import "sandbox-common";
|
||||
|
||||
:global .mo-standalone {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
|
|
@ -1,181 +1,185 @@
|
|||
@import "../scss/variables.scss";
|
||||
|
||||
.toolbar {
|
||||
float: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form { // This is here to give the styles below higher priority than Bootstrap has
|
||||
:global .DayPicker {
|
||||
border: $input-border-width solid $input-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
}
|
||||
|
||||
:global .form-horizontal .control-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global .form-control[disabled] {
|
||||
cursor: default;
|
||||
background-color: #eeeeee;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global .ace_editor {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.buttonRow:last-child {
|
||||
// This is to move Save/Delete buttons a bit down
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.staticFormGroup {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dayPickerWrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
}
|
||||
|
||||
.buttonRow > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.buttonRow > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.formDisabled {
|
||||
background-color: #eeeeee;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.formStatus {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.dataTableTable {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.actionLinks > * {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.actionLinks > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.tableSelectDropdown {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tableSelectTable.tableSelectTableHidden {
|
||||
display: none;
|
||||
height: 0px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.tableSelectDropdown input[readonly] {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
:global h3.legend {
|
||||
font-size: 21px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tertiaryNav {
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
|
||||
:global .nav-item .nav-link {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colorPickerSwatchWrapper {
|
||||
padding: 7px;
|
||||
background: #fff;
|
||||
border: 1px solid #AAB2BD;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
.colorPickerSwatchColor {
|
||||
width: 60px;
|
||||
height: 18px;
|
||||
borderRadius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.colorPickerWrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropZone{
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 3px;
|
||||
margin-top: 3px;
|
||||
border: 2px solid #E6E9ED;
|
||||
border-radius: 5px;
|
||||
background-color: #FAFAD2;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #808080;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropZoneActive{
|
||||
border-color: #90EE90;
|
||||
color: #000;
|
||||
background-color: #DDFFDD;
|
||||
}
|
||||
|
||||
|
||||
.untrustedContent {
|
||||
border: 0px none;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.withElementInFullscreen {
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iconDisabled {
|
||||
color: $link-color;
|
||||
text-decoration: $link-decoration;
|
||||
}
|
||||
|
||||
.dependenciesList {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
:global .modal-dialog {
|
||||
@media (min-width: 768px) {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
@import "../scss/variables.scss";
|
||||
|
||||
.toolbar {
|
||||
float: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form { // This is here to give the styles below higher priority than Bootstrap has
|
||||
:global .DayPicker {
|
||||
border: $input-border-width solid $input-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
}
|
||||
|
||||
:global .form-horizontal .control-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global .form-control[disabled] {
|
||||
cursor: default;
|
||||
background-color: #eeeeee;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global .ace_editor {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.buttonRow:last-child {
|
||||
// This is to move Save/Delete buttons a bit down
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.staticFormGroup {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dayPickerWrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
}
|
||||
|
||||
.buttonRow > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.buttonRow > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.formDisabled {
|
||||
background-color: #eeeeee;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.formStatus {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.dataTableTable {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.actionLinks > * {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.actionLinks > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.tableSelectDropdown {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tableSelectTable.tableSelectTableHidden {
|
||||
display: none;
|
||||
height: 0px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.tableSelectDropdown input[readonly] {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
:global h3.legend {
|
||||
font-size: 21px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tertiaryNav {
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
|
||||
:global .nav-item .nav-link {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colorPickerSwatchWrapper {
|
||||
padding: 7px;
|
||||
background: #fff;
|
||||
border: 1px solid #AAB2BD;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
.colorPickerSwatchColor {
|
||||
width: 60px;
|
||||
height: 18px;
|
||||
borderRadius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.colorPickerWrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.checkboxText{
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.dropZone{
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 3px;
|
||||
margin-top: 3px;
|
||||
border: 2px solid #E6E9ED;
|
||||
border-radius: 5px;
|
||||
background-color: #FAFAD2;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #808080;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropZoneActive{
|
||||
border-color: #90EE90;
|
||||
color: #000;
|
||||
background-color: #DDFFDD;
|
||||
}
|
||||
|
||||
|
||||
.untrustedContent {
|
||||
border: 0px none;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.withElementInFullscreen {
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iconDisabled {
|
||||
color: $link-color;
|
||||
text-decoration: $link-decoration;
|
||||
}
|
||||
|
||||
.dependenciesList {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
:global .modal-dialog {
|
||||
@media (min-width: 768px) {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,92 +1,92 @@
|
|||
@import "../scss/variables.scss";
|
||||
|
||||
:global {
|
||||
|
||||
.mt-treetable-container .fancytree-container {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-expander {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
|
||||
outline: 0px none;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.mt-treetable-container>table.fancytree-ext-table {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
|
||||
border-top: 0px none;
|
||||
}
|
||||
|
||||
.mt-treetable-container .mt-treetable-title {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: $input-border-width solid $input-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding-top: $input-padding-y;
|
||||
padding-bottom: $input-padding-y;
|
||||
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-valid {
|
||||
border-color: $form-feedback-valid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-invalid {
|
||||
border-color: $form-feedback-invalid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.mt-treetable-container .table td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
@import "../scss/variables.scss";
|
||||
|
||||
:global {
|
||||
|
||||
.mt-treetable-container .fancytree-container {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-expander {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
|
||||
outline: 0px none;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.mt-treetable-container>table.fancytree-ext-table {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
|
||||
border-top: 0px none;
|
||||
}
|
||||
|
||||
.mt-treetable-container .mt-treetable-title {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: $input-border-width solid $input-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding-top: $input-padding-y;
|
||||
padding-bottom: $input-padding-y;
|
||||
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-valid {
|
||||
border-color: $form-feedback-valid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-invalid {
|
||||
border-color: $form-feedback-invalid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.mt-treetable-container .table td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,60 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
|
||||
import {AppType} from '../../../shared/app';
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
import i18n from './i18n';
|
||||
|
||||
let restrictedAccessToken = anonymousRestrictedAccessToken;
|
||||
|
||||
function setRestrictedAccessToken(token) {
|
||||
restrictedAccessToken = token;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return mailtrainConfig.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path, customRestrictedAccessToken) {
|
||||
const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
|
||||
return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
|
||||
}
|
||||
|
||||
function getPublicUrl(path, opts) {
|
||||
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
|
||||
|
||||
if (opts && opts.withLocale) {
|
||||
url.searchParams.append('locale', i18n.language);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getUrl(path) {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return getTrustedUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return getSandboxUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return getPublicUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseDir() {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return mailtrainConfig.trustedUrlBaseDir;
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return mailtrainConfig.publicUrlBaseDir;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getPublicUrl,
|
||||
getUrl,
|
||||
getBaseDir,
|
||||
setRestrictedAccessToken
|
||||
'use strict';
|
||||
|
||||
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
|
||||
import {AppType} from '../../../shared/app';
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
import i18n from './i18n';
|
||||
|
||||
let restrictedAccessToken = anonymousRestrictedAccessToken;
|
||||
|
||||
function setRestrictedAccessToken(token) {
|
||||
restrictedAccessToken = token;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return mailtrainConfig.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path, customRestrictedAccessToken) {
|
||||
const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
|
||||
return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
|
||||
}
|
||||
|
||||
function getPublicUrl(path, opts) {
|
||||
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
|
||||
|
||||
if (opts && opts.withLocale) {
|
||||
url.searchParams.append('locale', i18n.language);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getUrl(path) {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return getTrustedUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return getSandboxUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return getPublicUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseDir() {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return mailtrainConfig.trustedUrlBaseDir;
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return mailtrainConfig.publicUrlBaseDir;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getPublicUrl,
|
||||
getUrl,
|
||||
getBaseDir,
|
||||
setRestrictedAccessToken
|
||||
}
|
|
@ -1,55 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {Fieldset, InputField} from "../../lib/form";
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const fieldTypes = {
|
||||
text: {
|
||||
label: t('text'),
|
||||
},
|
||||
website: {
|
||||
label: t('website'),
|
||||
},
|
||||
longtext: {
|
||||
label: t('multilineText'),
|
||||
},
|
||||
gpg: {
|
||||
label: t('gpgPublicKey'),
|
||||
},
|
||||
number: {
|
||||
label: t('number'),
|
||||
},
|
||||
'checkbox-grouped': {
|
||||
label: t('checkboxesFromOptionFields'),
|
||||
},
|
||||
'radio-grouped': {
|
||||
label: t('radioButtonsFromOptionFields')
|
||||
},
|
||||
'dropdown-grouped': {
|
||||
label: t('dropDownFromOptionFields')
|
||||
},
|
||||
'radio-enum': {
|
||||
label: t('radioButtonsEnumerated')
|
||||
},
|
||||
'dropdown-enum': {
|
||||
label: t('dropDownEnumerated')
|
||||
},
|
||||
'date': {
|
||||
label: t('date')
|
||||
},
|
||||
'birthday': {
|
||||
label: t('birthday')
|
||||
},
|
||||
json: {
|
||||
label: t('jsonValueForCustomRendering')
|
||||
},
|
||||
option: {
|
||||
label: t('option')
|
||||
}
|
||||
};
|
||||
|
||||
return fieldTypes;
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {Fieldset, InputField} from "../../lib/form";
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const fieldTypes = {
|
||||
text: {
|
||||
label: t('text'),
|
||||
},
|
||||
website: {
|
||||
label: t('website'),
|
||||
},
|
||||
longtext: {
|
||||
label: t('multilineText'),
|
||||
},
|
||||
gpg: {
|
||||
label: t('gpgPublicKey'),
|
||||
},
|
||||
number: {
|
||||
label: t('number'),
|
||||
},
|
||||
'checkbox-grouped': {
|
||||
label: t('checkboxesFromOptionFields'),
|
||||
},
|
||||
'radio-grouped': {
|
||||
label: t('radioButtonsFromOptionFields')
|
||||
},
|
||||
'dropdown-grouped': {
|
||||
label: t('dropDownFromOptionFields')
|
||||
},
|
||||
'radio-enum': {
|
||||
label: t('radioButtonsEnumerated')
|
||||
},
|
||||
'dropdown-enum': {
|
||||
label: t('dropDownEnumerated')
|
||||
},
|
||||
'date': {
|
||||
label: t('date')
|
||||
},
|
||||
'birthday': {
|
||||
label: t('birthday')
|
||||
},
|
||||
json: {
|
||||
label: t('jsonValueForCustomRendering')
|
||||
},
|
||||
option: {
|
||||
label: t('option')
|
||||
}
|
||||
};
|
||||
|
||||
return fieldTypes;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
$editorNormalHeight: 400px;
|
||||
@import "../../lib/sandbox-common";
|
||||
|
||||
.editor {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.host {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
$editorNormalHeight: 400px;
|
||||
@import "../../lib/sandbox-common";
|
||||
|
||||
.editor {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.host {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../shared/imports';
|
||||
|
||||
export function getImportLabels(t) {
|
||||
|
||||
const importSourceLabels = {
|
||||
[ImportSource.CSV_FILE]: t('csvFile'),
|
||||
[ImportSource.LIST]: t('list'),
|
||||
};
|
||||
|
||||
const importStatusLabels = {
|
||||
[ImportStatus.PREP_SCHEDULED]: t('created'),
|
||||
[ImportStatus.PREP_RUNNING]: t('preparing'),
|
||||
[ImportStatus.PREP_STOPPING]: t('stopping'),
|
||||
[ImportStatus.PREP_FINISHED]: t('ready'),
|
||||
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
|
||||
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
|
||||
[ImportStatus.RUN_RUNNING]: t('running'),
|
||||
[ImportStatus.RUN_STOPPING]: t('stopping'),
|
||||
[ImportStatus.RUN_FINISHED]: t('finished'),
|
||||
[ImportStatus.RUN_FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const runStatusLabels = {
|
||||
[RunStatus.SCHEDULED]: t('starting'),
|
||||
[RunStatus.RUNNING]: t('running'),
|
||||
[RunStatus.STOPPING]: t('stopping'),
|
||||
[RunStatus.FINISHED]: t('finished'),
|
||||
[RunStatus.FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const mappingTypeLabels = {
|
||||
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
|
||||
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
|
||||
}
|
||||
|
||||
return {
|
||||
importStatusLabels,
|
||||
mappingTypeLabels,
|
||||
importSourceLabels,
|
||||
runStatusLabels
|
||||
};
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../shared/imports';
|
||||
|
||||
export function getImportLabels(t) {
|
||||
|
||||
const importSourceLabels = {
|
||||
[ImportSource.CSV_FILE]: t('csvFile'),
|
||||
[ImportSource.LIST]: t('list'),
|
||||
};
|
||||
|
||||
const importStatusLabels = {
|
||||
[ImportStatus.PREP_SCHEDULED]: t('created'),
|
||||
[ImportStatus.PREP_RUNNING]: t('preparing'),
|
||||
[ImportStatus.PREP_STOPPING]: t('stopping'),
|
||||
[ImportStatus.PREP_FINISHED]: t('ready'),
|
||||
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
|
||||
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
|
||||
[ImportStatus.RUN_RUNNING]: t('running'),
|
||||
[ImportStatus.RUN_STOPPING]: t('stopping'),
|
||||
[ImportStatus.RUN_FINISHED]: t('finished'),
|
||||
[ImportStatus.RUN_FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const runStatusLabels = {
|
||||
[RunStatus.SCHEDULED]: t('starting'),
|
||||
[RunStatus.RUNNING]: t('running'),
|
||||
[RunStatus.STOPPING]: t('stopping'),
|
||||
[RunStatus.FINISHED]: t('finished'),
|
||||
[RunStatus.FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const mappingTypeLabels = {
|
||||
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
|
||||
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
|
||||
}
|
||||
|
||||
return {
|
||||
importStatusLabels,
|
||||
mappingTypeLabels,
|
||||
importSourceLabels,
|
||||
runStatusLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,152 +1,152 @@
|
|||
$desktopMinWidth: 768px;
|
||||
|
||||
$mobileLeftPaneResidualWidth: 0px;
|
||||
$mobileAnimationStartPosition: 100px;
|
||||
|
||||
$desktopLeftPaneResidualWidth: 200px;
|
||||
$desktopAnimationStartPosition: 300px;
|
||||
|
||||
@mixin optionsHidden {
|
||||
transform: translateX($mobileAnimationStartPosition);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopAnimationStartPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin optionsVisible {
|
||||
transform: translateX($mobileLeftPaneResidualWidth);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopLeftPaneResidualWidth);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ruleActionLink {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.rulePane {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.leftPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
|
||||
.leftPaneInner {
|
||||
.ruleTree {
|
||||
background: #fbfbfb;
|
||||
border: #cfcfcf 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
// Without this, the placeholders when rearranging the tree are not shown
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leftPaneOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
width: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
width: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('./divider.png') repeat-y;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
padding-left: 50px;
|
||||
z-index: 1;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.paneDividerSolidBackground {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.rightPaneInner {
|
||||
margin-right: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
margin-right: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
|
||||
.ruleOptions {
|
||||
margin-left: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsVisible {
|
||||
.leftPaneOverlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsHidden {
|
||||
.paneDivider {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
$desktopMinWidth: 768px;
|
||||
|
||||
$mobileLeftPaneResidualWidth: 0px;
|
||||
$mobileAnimationStartPosition: 100px;
|
||||
|
||||
$desktopLeftPaneResidualWidth: 200px;
|
||||
$desktopAnimationStartPosition: 300px;
|
||||
|
||||
@mixin optionsHidden {
|
||||
transform: translateX($mobileAnimationStartPosition);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopAnimationStartPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin optionsVisible {
|
||||
transform: translateX($mobileLeftPaneResidualWidth);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopLeftPaneResidualWidth);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ruleActionLink {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.rulePane {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.leftPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
|
||||
.leftPaneInner {
|
||||
.ruleTree {
|
||||
background: #fbfbfb;
|
||||
border: #cfcfcf 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
// Without this, the placeholders when rearranging the tree are not shown
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leftPaneOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
width: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
width: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('./divider.png') repeat-y;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
padding-left: 50px;
|
||||
z-index: 1;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.paneDividerSolidBackground {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.rightPaneInner {
|
||||
margin-right: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
margin-right: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
|
||||
.ruleOptions {
|
||||
margin-left: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsVisible {
|
||||
.leftPaneOverlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsHidden {
|
||||
.paneDivider {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,459 +1,459 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {DatePicker, Dropdown, InputField} from "../../lib/form";
|
||||
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
|
||||
import { tMark } from "../../lib/i18n";
|
||||
|
||||
export function getRuleHelpers(t, fields) {
|
||||
|
||||
const ruleHelpers = {};
|
||||
|
||||
ruleHelpers.compositeRuleTypes = {
|
||||
all: {
|
||||
dropdownLabel: t('allRulesMustMatch'),
|
||||
treeLabel: rule => t('allRulesMustMatch')
|
||||
},
|
||||
some: {
|
||||
dropdownLabel: t('atLeastOneRuleMustMatch'),
|
||||
treeLabel: rule => t('atLeastOneRuleMustMatch')
|
||||
},
|
||||
none: {
|
||||
dropdownLabel: t('noRuleMayMatch'),
|
||||
treeLabel: rule => t('noRuleMayMatch')
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes = {};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.text = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('alphabeticallyBefore'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('alphabeticallyAfter'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.website = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.number = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('lessThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('lessThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('greaterThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('greaterThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
// FXIME - the localization here is still wrong
|
||||
function getRelativeDateTreeLabel(rule, variants) {
|
||||
if (rule.value === 0) {
|
||||
return t(variants[0], {colName: ruleHelpers.getColumnName(rule.column)})
|
||||
} else if (rule.value > 0) {
|
||||
return t(variants[1], {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
|
||||
} else {
|
||||
return t(variants[2], {colName: ruleHelpers.getColumnName(rule.column), value: -rule.value});
|
||||
}
|
||||
}
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.date = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
eqTodayPlusDays: {
|
||||
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsTheCurrentDate'), tMark('dateInColumnColNameIsTheValuethDayAfter'), tMark('dateInColumnColNameIsTheValuethDayBefore')]),
|
||||
},
|
||||
ltTodayPlusDays: {
|
||||
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeTheCurrent'), tMark('dateInColumnColNameIsBeforeTheValuethDay'), tMark('dateInColumnColNameIsBeforeTheValuethDay-1')]),
|
||||
},
|
||||
leTodayPlusDays: {
|
||||
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeOrOnThe'), tMark('dateInColumnColNameIsBeforeOrOnThe-1'), tMark('dateInColumnColNameIsBeforeOrOnThe-2')]),
|
||||
},
|
||||
gtTodayPlusDays: {
|
||||
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterTheCurrentDate'), tMark('dateInColumnColNameIsAfterTheValuethDay'), tMark('dateInColumnColNameIsAfterTheValuethDay-1')]),
|
||||
},
|
||||
geTodayPlusDays: {
|
||||
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterOrOnTheCurrent'), tMark('dateInColumnColNameIsAfterOrOnTheValueth'), tMark('dateInColumnColNameIsAfterOrOnTheValueth-1')]),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.birthday = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.option = {
|
||||
isTrue: {
|
||||
dropdownLabel: t('isSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
},
|
||||
isFalse: {
|
||||
dropdownLabel: t('isNotSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
|
||||
eq: {
|
||||
dropdownLabel: t('keyEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('keyMatchWithSqlLike'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('keyMatchWithRegularExpressions'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('keyAlphabeticallyBefore'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfter'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const stringValueSettings = allowEmpty => ({
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = getter('value');
|
||||
},
|
||||
validate: state => {
|
||||
if (!allowEmpty && !state.getIn(['value', 'value'])) {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const numberValueSettings = {
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value.toString()
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseInt(getter('value'));
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['value', 'value']).trim();
|
||||
if (value === '') {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['value', 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const birthdayValueSettings = {
|
||||
getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
|
||||
getFormData: rule => ({
|
||||
birthday: formatBirthday(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseBirthday(DateFormat.INTL, getter('birthday')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['birthday', 'value']);
|
||||
const date = parseBirthday(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['birthday', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateValueSettings = {
|
||||
getForm: () => <DatePicker id="date" label={t('date')} />,
|
||||
getFormData: rule => ({
|
||||
date: formatDate(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseDate(DateFormat.INTL, getter('date')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['date', 'value']);
|
||||
const date = parseDate(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['date', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['date', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateRelativeValueSettings = {
|
||||
getForm: () =>
|
||||
<div>
|
||||
<InputField id="daysValue" label={t('numberOfDays')}/>
|
||||
<Dropdown id="direction" label={t('beforeAfter')} options={[
|
||||
{ key: 'before', label: t('beforeCurrentDate') },
|
||||
{ key: 'after', label: t('afterCurrentDate') }
|
||||
]}/>
|
||||
</div>,
|
||||
getFormData: rule => ({
|
||||
daysValue: Math.abs(rule.value).toString(),
|
||||
direction: rule.value >= 0 ? 'after' : 'before'
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
const direction = getter('direction');
|
||||
rule.value = parseInt(getter('daysValue')) * (direction === 'before' ? -1 : 1);
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['daysValue', 'value']);
|
||||
if (!value) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['daysValue', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const optionValueSettings = {
|
||||
getForm: () => null,
|
||||
getFormData: rule => ({}),
|
||||
assignRuleSettings: (rule, getter) => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
|
||||
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
|
||||
for (const key of keys) {
|
||||
Object.assign(ruleTypes[key], settings);
|
||||
}
|
||||
}
|
||||
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, ['eq', 'lt', 'le', 'gt', 'ge'], numberValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
|
||||
ruleHelpers.primitiveRuleTypesFormDataDefaults = {
|
||||
value: '',
|
||||
date: '',
|
||||
daysValue: '',
|
||||
birthday: '',
|
||||
direction: 'before'
|
||||
};
|
||||
|
||||
|
||||
|
||||
ruleHelpers.getCompositeRuleTypeOptions = () => {
|
||||
const order = ['all', 'some', 'none'];
|
||||
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
|
||||
};
|
||||
|
||||
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
|
||||
const order = {
|
||||
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
website: ['eq', 'like', 're'],
|
||||
number: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
|
||||
option: ['isTrue', 'isFalse'],
|
||||
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
|
||||
};
|
||||
|
||||
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
|
||||
};
|
||||
|
||||
const predefColumns = [
|
||||
{
|
||||
column: 'email',
|
||||
name: t('emailAddress-1'),
|
||||
type: 'text',
|
||||
key: 'EMAIL'
|
||||
},
|
||||
{
|
||||
column: 'opt_in_country',
|
||||
name: t('signupCountry'),
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
column: 'created',
|
||||
name: t('signUpDate'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_open',
|
||||
name: t('latestOpen'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_click',
|
||||
name: t('latestClick'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'is_test',
|
||||
name: t('Test user'),
|
||||
type: 'option'
|
||||
}
|
||||
];
|
||||
|
||||
ruleHelpers.fields = [
|
||||
...predefColumns,
|
||||
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
|
||||
];
|
||||
|
||||
ruleHelpers.fieldsByColumn = {};
|
||||
for (const fld of ruleHelpers.fields) {
|
||||
ruleHelpers.fieldsByColumn[fld.column] = fld;
|
||||
}
|
||||
|
||||
ruleHelpers.getColumnType = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.type;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getColumnName = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.name;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getRuleTypeSettings = rule => {
|
||||
if (ruleHelpers.isCompositeRuleType(rule.type)) {
|
||||
return ruleHelpers.compositeRuleTypes[rule.type];
|
||||
} else {
|
||||
const colType = ruleHelpers.getColumnType(rule.column);
|
||||
|
||||
if (colType) {
|
||||
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
|
||||
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
|
||||
|
||||
return ruleHelpers;
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {DatePicker, Dropdown, InputField} from "../../lib/form";
|
||||
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
|
||||
import { tMark } from "../../lib/i18n";
|
||||
|
||||
export function getRuleHelpers(t, fields) {
|
||||
|
||||
const ruleHelpers = {};
|
||||
|
||||
ruleHelpers.compositeRuleTypes = {
|
||||
all: {
|
||||
dropdownLabel: t('allRulesMustMatch'),
|
||||
treeLabel: rule => t('allRulesMustMatch')
|
||||
},
|
||||
some: {
|
||||
dropdownLabel: t('atLeastOneRuleMustMatch'),
|
||||
treeLabel: rule => t('atLeastOneRuleMustMatch')
|
||||
},
|
||||
none: {
|
||||
dropdownLabel: t('noRuleMayMatch'),
|
||||
treeLabel: rule => t('noRuleMayMatch')
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes = {};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.text = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('alphabeticallyBefore'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('alphabeticallyAfter'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.website = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.number = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('lessThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('lessThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('greaterThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('greaterThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
// FXIME - the localization here is still wrong
|
||||
function getRelativeDateTreeLabel(rule, variants) {
|
||||
if (rule.value === 0) {
|
||||
return t(variants[0], {colName: ruleHelpers.getColumnName(rule.column)})
|
||||
} else if (rule.value > 0) {
|
||||
return t(variants[1], {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
|
||||
} else {
|
||||
return t(variants[2], {colName: ruleHelpers.getColumnName(rule.column), value: -rule.value});
|
||||
}
|
||||
}
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.date = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
eqTodayPlusDays: {
|
||||
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsTheCurrentDate'), tMark('dateInColumnColNameIsTheValuethDayAfter'), tMark('dateInColumnColNameIsTheValuethDayBefore')]),
|
||||
},
|
||||
ltTodayPlusDays: {
|
||||
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeTheCurrent'), tMark('dateInColumnColNameIsBeforeTheValuethDay'), tMark('dateInColumnColNameIsBeforeTheValuethDay-1')]),
|
||||
},
|
||||
leTodayPlusDays: {
|
||||
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeOrOnThe'), tMark('dateInColumnColNameIsBeforeOrOnThe-1'), tMark('dateInColumnColNameIsBeforeOrOnThe-2')]),
|
||||
},
|
||||
gtTodayPlusDays: {
|
||||
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterTheCurrentDate'), tMark('dateInColumnColNameIsAfterTheValuethDay'), tMark('dateInColumnColNameIsAfterTheValuethDay-1')]),
|
||||
},
|
||||
geTodayPlusDays: {
|
||||
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterOrOnTheCurrent'), tMark('dateInColumnColNameIsAfterOrOnTheValueth'), tMark('dateInColumnColNameIsAfterOrOnTheValueth-1')]),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.birthday = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.option = {
|
||||
isTrue: {
|
||||
dropdownLabel: t('isSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
},
|
||||
isFalse: {
|
||||
dropdownLabel: t('isNotSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
|
||||
eq: {
|
||||
dropdownLabel: t('keyEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('keyMatchWithSqlLike'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('keyMatchWithRegularExpressions'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('keyAlphabeticallyBefore'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfter'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const stringValueSettings = allowEmpty => ({
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = getter('value');
|
||||
},
|
||||
validate: state => {
|
||||
if (!allowEmpty && !state.getIn(['value', 'value'])) {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const numberValueSettings = {
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value.toString()
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseInt(getter('value'));
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['value', 'value']).trim();
|
||||
if (value === '') {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['value', 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const birthdayValueSettings = {
|
||||
getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
|
||||
getFormData: rule => ({
|
||||
birthday: formatBirthday(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseBirthday(DateFormat.INTL, getter('birthday')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['birthday', 'value']);
|
||||
const date = parseBirthday(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['birthday', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateValueSettings = {
|
||||
getForm: () => <DatePicker id="date" label={t('date')} />,
|
||||
getFormData: rule => ({
|
||||
date: formatDate(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseDate(DateFormat.INTL, getter('date')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['date', 'value']);
|
||||
const date = parseDate(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['date', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['date', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateRelativeValueSettings = {
|
||||
getForm: () =>
|
||||
<div>
|
||||
<InputField id="daysValue" label={t('numberOfDays')}/>
|
||||
<Dropdown id="direction" label={t('beforeAfter')} options={[
|
||||
{ key: 'before', label: t('beforeCurrentDate') },
|
||||
{ key: 'after', label: t('afterCurrentDate') }
|
||||
]}/>
|
||||
</div>,
|
||||
getFormData: rule => ({
|
||||
daysValue: Math.abs(rule.value).toString(),
|
||||
direction: rule.value >= 0 ? 'after' : 'before'
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
const direction = getter('direction');
|
||||
rule.value = parseInt(getter('daysValue')) * (direction === 'before' ? -1 : 1);
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['daysValue', 'value']);
|
||||
if (!value) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['daysValue', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const optionValueSettings = {
|
||||
getForm: () => null,
|
||||
getFormData: rule => ({}),
|
||||
assignRuleSettings: (rule, getter) => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
|
||||
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
|
||||
for (const key of keys) {
|
||||
Object.assign(ruleTypes[key], settings);
|
||||
}
|
||||
}
|
||||
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, ['eq', 'lt', 'le', 'gt', 'ge'], numberValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
|
||||
ruleHelpers.primitiveRuleTypesFormDataDefaults = {
|
||||
value: '',
|
||||
date: '',
|
||||
daysValue: '',
|
||||
birthday: '',
|
||||
direction: 'before'
|
||||
};
|
||||
|
||||
|
||||
|
||||
ruleHelpers.getCompositeRuleTypeOptions = () => {
|
||||
const order = ['all', 'some', 'none'];
|
||||
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
|
||||
};
|
||||
|
||||
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
|
||||
const order = {
|
||||
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
website: ['eq', 'like', 're'],
|
||||
number: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
|
||||
option: ['isTrue', 'isFalse'],
|
||||
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
|
||||
};
|
||||
|
||||
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
|
||||
};
|
||||
|
||||
const predefColumns = [
|
||||
{
|
||||
column: 'email',
|
||||
name: t('emailAddress-1'),
|
||||
type: 'text',
|
||||
key: 'EMAIL'
|
||||
},
|
||||
{
|
||||
column: 'opt_in_country',
|
||||
name: t('signupCountry'),
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
column: 'created',
|
||||
name: t('signUpDate'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_open',
|
||||
name: t('latestOpen'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_click',
|
||||
name: t('latestClick'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'is_test',
|
||||
name: t('Test user'),
|
||||
type: 'option'
|
||||
}
|
||||
];
|
||||
|
||||
ruleHelpers.fields = [
|
||||
...predefColumns,
|
||||
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
|
||||
];
|
||||
|
||||
ruleHelpers.fieldsByColumn = {};
|
||||
for (const fld of ruleHelpers.fields) {
|
||||
ruleHelpers.fieldsByColumn[fld.column] = fld;
|
||||
}
|
||||
|
||||
ruleHelpers.getColumnType = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.type;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getColumnName = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.name;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getRuleTypeSettings = rule => {
|
||||
if (ruleHelpers.isCompositeRuleType(rule.type)) {
|
||||
return ruleHelpers.compositeRuleTypes[rule.type];
|
||||
} else {
|
||||
const colType = ruleHelpers.getColumnType(rule.column);
|
||||
|
||||
if (colType) {
|
||||
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
|
||||
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
|
||||
|
||||
return ruleHelpers;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.mapping {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.erased {
|
||||
color: #808080;
|
||||
.mapping {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.erased {
|
||||
color: #808080;
|
||||
}
|
|
@ -1,212 +1,212 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {SubscriptionStatus} from "../../../../shared/lists";
|
||||
import {
|
||||
ACEEditor,
|
||||
CheckBox,
|
||||
CheckBoxGroup,
|
||||
DatePicker,
|
||||
Dropdown,
|
||||
InputField,
|
||||
RadioGroup,
|
||||
TextArea
|
||||
} from "../../lib/form";
|
||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||
import {getFieldColumn} from '../../../../shared/lists';
|
||||
import 'brace/mode/json';
|
||||
|
||||
export function getSubscriptionStatusLabels(t) {
|
||||
|
||||
const subscriptionStatusLabels = {
|
||||
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
|
||||
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
|
||||
[SubscriptionStatus.BOUNCED]: t('bounced'),
|
||||
[SubscriptionStatus.COMPLAINED]: t('complained'),
|
||||
};
|
||||
|
||||
return subscriptionStatusLabels;
|
||||
}
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const groupedFieldTypes = {};
|
||||
|
||||
const stringFieldType = long => ({
|
||||
form: groupedField => long ? <TextArea key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/> : <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
});
|
||||
|
||||
const numberFieldType = {
|
||||
form: groupedField => <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? value.toString() : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = parseInt(data[getFieldColumn(groupedField)]);
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
|
||||
if (value !== '' && isNaN(value)) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const dateFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatDate(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseDate(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseDate(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const birthdayFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} birthday />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatBirthday(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const jsonFieldType = {
|
||||
form: groupedField => <ACEEditor key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
};
|
||||
|
||||
const optionFieldType = {
|
||||
form: groupedField => <CheckBox key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} text={groupedField.settings.checkedLabel} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = !!value;
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = false;
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const enumSingleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
const enumMultipleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
|
||||
groupedFieldTypes.text = stringFieldType(false);
|
||||
groupedFieldTypes.website = stringFieldType(false);
|
||||
groupedFieldTypes.longtext = stringFieldType(true);
|
||||
groupedFieldTypes.gpg = stringFieldType(true);
|
||||
groupedFieldTypes.number = numberFieldType;
|
||||
groupedFieldTypes.date = dateFieldType;
|
||||
groupedFieldTypes.birthday = birthdayFieldType;
|
||||
groupedFieldTypes.json = jsonFieldType;
|
||||
groupedFieldTypes.option = optionFieldType;
|
||||
groupedFieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
|
||||
groupedFieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
|
||||
|
||||
// Here we rely on the fact the model/groupedFields and model/subscriptions preprocess the groupedField info and subscription
|
||||
// such that the grouped entries behave the same as the enum entries
|
||||
groupedFieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
|
||||
groupedFieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
|
||||
groupedFieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
|
||||
|
||||
return groupedFieldTypes;
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {SubscriptionStatus} from "../../../../shared/lists";
|
||||
import {
|
||||
ACEEditor,
|
||||
CheckBox,
|
||||
CheckBoxGroup,
|
||||
DatePicker,
|
||||
Dropdown,
|
||||
InputField,
|
||||
RadioGroup,
|
||||
TextArea
|
||||
} from "../../lib/form";
|
||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||
import {getFieldColumn} from '../../../../shared/lists';
|
||||
import 'brace/mode/json';
|
||||
|
||||
export function getSubscriptionStatusLabels(t) {
|
||||
|
||||
const subscriptionStatusLabels = {
|
||||
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
|
||||
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
|
||||
[SubscriptionStatus.BOUNCED]: t('bounced'),
|
||||
[SubscriptionStatus.COMPLAINED]: t('complained'),
|
||||
};
|
||||
|
||||
return subscriptionStatusLabels;
|
||||
}
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const groupedFieldTypes = {};
|
||||
|
||||
const stringFieldType = long => ({
|
||||
form: groupedField => long ? <TextArea key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/> : <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
});
|
||||
|
||||
const numberFieldType = {
|
||||
form: groupedField => <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? value.toString() : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = parseInt(data[getFieldColumn(groupedField)]);
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
|
||||
if (value !== '' && isNaN(value)) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const dateFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatDate(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseDate(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseDate(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const birthdayFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} birthday />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatBirthday(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const jsonFieldType = {
|
||||
form: groupedField => <ACEEditor key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
};
|
||||
|
||||
const optionFieldType = {
|
||||
form: groupedField => <CheckBox key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} text={groupedField.settings.checkedLabel} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = !!value;
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = false;
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const enumSingleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
const enumMultipleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
|
||||
groupedFieldTypes.text = stringFieldType(false);
|
||||
groupedFieldTypes.website = stringFieldType(false);
|
||||
groupedFieldTypes.longtext = stringFieldType(true);
|
||||
groupedFieldTypes.gpg = stringFieldType(true);
|
||||
groupedFieldTypes.number = numberFieldType;
|
||||
groupedFieldTypes.date = dateFieldType;
|
||||
groupedFieldTypes.birthday = birthdayFieldType;
|
||||
groupedFieldTypes.json = jsonFieldType;
|
||||
groupedFieldTypes.option = optionFieldType;
|
||||
groupedFieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
|
||||
groupedFieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
|
||||
|
||||
// Here we rely on the fact the model/groupedFields and model/subscriptions preprocess the groupedField info and subscription
|
||||
// such that the grouped entries behave the same as the enum entries
|
||||
groupedFieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
|
||||
groupedFieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
|
||||
groupedFieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
|
||||
|
||||
return groupedFieldTypes;
|
||||
}
|
|
@ -108,7 +108,7 @@ class Root extends Component {
|
|||
<ul className="navbar-nav mt-navbar-nav-left">
|
||||
{topLevelMenu}
|
||||
<NavDropdown label={t('administration')}>
|
||||
<DropdownLink to="/users">{t('users')}</DropdownLink>
|
||||
{mailtrainConfig.globalPermissions.displayManageUsers && <DropdownLink to="/users">{t('users')}</DropdownLink>}
|
||||
<DropdownLink to="/namespaces">{t('namespaces')}</DropdownLink>
|
||||
{mailtrainConfig.globalPermissions.manageSettings && <DropdownLink to="/settings">{t('globalSettings')}</DropdownLink>}
|
||||
<DropdownLink to="/send-configurations">{t('sendConfigurations')}</DropdownLink>
|
||||
|
|
|
@ -40,6 +40,8 @@ import {
|
|||
import styles
|
||||
from "../lib/styles.scss";
|
||||
|
||||
import sendConfigurationsStyles from "./styles.scss";
|
||||
|
||||
import mailtrainConfig
|
||||
from 'mailtrainConfig';
|
||||
import {withComponentMixins} from "../lib/decorator-helpers";
|
||||
|
@ -231,13 +233,13 @@ export default class CUD extends Component {
|
|||
|
||||
<Fieldset label={t('emailHeader')}>
|
||||
<InputField id="from_email" label={t('defaultFromEmail')}/>
|
||||
<CheckBox id="from_email_overridable" text={t('overridable')}/>
|
||||
<CheckBox id="from_email_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
|
||||
<InputField id="from_name" label={t('defaultFromName')}/>
|
||||
<CheckBox id="from_name_overridable" text={t('overridable')}/>
|
||||
<CheckBox id="from_name_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
|
||||
<InputField id="reply_to" label={t('defaultReplytoEmail')}/>
|
||||
<CheckBox id="reply_to_overridable" text={t('overridable')}/>
|
||||
<CheckBox id="reply_to_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
|
||||
<InputField id="subject" label={t('subject')}/>
|
||||
<CheckBox id="subject_overridable" text={t('overridable')}/>
|
||||
<CheckBox id="subject_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
|
||||
<InputField id="x_mailer" label={t('xMailer')}/>
|
||||
</Fieldset>
|
||||
|
||||
|
|
|
@ -1,337 +1,337 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
|
||||
import {
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
InputField,
|
||||
TextArea
|
||||
} from "../lib/form";
|
||||
import {Trans} from "react-i18next";
|
||||
import styles from "./styles.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
export const mailerTypesOrder = [
|
||||
MailerType.ZONE_MTA,
|
||||
MailerType.GENERIC_SMTP,
|
||||
MailerType.AWS_SES
|
||||
];
|
||||
|
||||
export function getMailerTypes(t) {
|
||||
const mailerTypes = {};
|
||||
|
||||
function initFieldsIfMissing(mutStateData, mailerType) {
|
||||
const initVals = mailerTypes[mailerType].initData();
|
||||
|
||||
for (const key in initVals) {
|
||||
if (!mutStateData.hasIn([key])) {
|
||||
mutStateData.setIn([key, 'value'], initVals[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeSave(data) {
|
||||
for (const mailerKey in mailerTypes) {
|
||||
const initVals = mailerTypes[mailerKey].initData();
|
||||
for (const fieldKey in initVals) {
|
||||
delete data[fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumber(state, field, label, emptyAllowed = false) {
|
||||
const value = state.getIn([field, 'value']);
|
||||
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
|
||||
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
|
||||
} else {
|
||||
state.setIn([field, 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
function getInitCommon() {
|
||||
return {
|
||||
maxConnections: '5',
|
||||
throttling: '',
|
||||
logTransactions: false
|
||||
};
|
||||
}
|
||||
|
||||
function getInitGenericSMTP() {
|
||||
return {
|
||||
...getInitCommon(),
|
||||
smtpHostname: '',
|
||||
smtpPort: '',
|
||||
smtpEncryption: 'NONE',
|
||||
smtpUseAuth: false,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpAllowSelfSigned: false,
|
||||
smtpMaxMessages: '100'
|
||||
};
|
||||
}
|
||||
|
||||
function afterLoadCommon(data) {
|
||||
data.maxConnections = data.mailer_settings.maxConnections;
|
||||
data.throttling = data.mailer_settings.throttling || '';
|
||||
data.logTransactions = data.mailer_settings.logTransactions;
|
||||
}
|
||||
|
||||
function afterLoadGenericSMTP(data) {
|
||||
afterLoadCommon(data);
|
||||
data.smtpHostname = data.mailer_settings.hostname;
|
||||
data.smtpPort = data.mailer_settings.port || '';
|
||||
data.smtpEncryption = data.mailer_settings.encryption;
|
||||
data.smtpUseAuth = data.mailer_settings.useAuth;
|
||||
data.smtpUser = data.mailer_settings.user;
|
||||
data.smtpPassword = data.mailer_settings.password;
|
||||
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
|
||||
data.smtpMaxMessages = data.mailer_settings.maxMessages;
|
||||
}
|
||||
|
||||
function beforeSaveCommon(data) {
|
||||
data.mailer_settings = {};
|
||||
data.mailer_settings.maxConnections = Number(data.maxConnections);
|
||||
data.mailer_settings.throttling = Number(data.throttling);
|
||||
data.mailer_settings.logTransactions = data.logTransactions;
|
||||
}
|
||||
|
||||
function beforeSaveGenericSMTP(data, builtin = false) {
|
||||
beforeSaveCommon(data);
|
||||
|
||||
if (!builtin) {
|
||||
data.mailer_settings.hostname = data.smtpHostname;
|
||||
data.mailer_settings.port = Number(data.smtpPort);
|
||||
data.mailer_settings.encryption = data.smtpEncryption;
|
||||
data.mailer_settings.useAuth = data.smtpUseAuth;
|
||||
data.mailer_settings.user = data.smtpUser;
|
||||
data.mailer_settings.password = data.smtpPassword;
|
||||
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
|
||||
}
|
||||
|
||||
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
|
||||
}
|
||||
|
||||
function validateCommon(state) {
|
||||
validateNumber(state, 'maxConnections', 'Max connections');
|
||||
validateNumber(state, 'throttling', 'Throttling', true);
|
||||
}
|
||||
|
||||
function validateGenericSMTP(state) {
|
||||
validateCommon(state);
|
||||
validateNumber(state, 'smtpPort', 'Port', true);
|
||||
validateNumber(state, 'smtpMaxMessages', 'Max messages');
|
||||
}
|
||||
|
||||
const typeNames = {
|
||||
[MailerType.GENERIC_SMTP]: t('genericSmtp'),
|
||||
[MailerType.ZONE_MTA]: t('zoneMta'),
|
||||
[MailerType.AWS_SES]: t('amazonSes')
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ key: MailerType.GENERIC_SMTP, label: typeNames[MailerType.GENERIC_SMTP]},
|
||||
{ key: MailerType.ZONE_MTA, label: typeNames[MailerType.ZONE_MTA]},
|
||||
{ key: MailerType.AWS_SES, label: typeNames[MailerType.AWS_SES]}
|
||||
];
|
||||
|
||||
const smtpEncryptionOptions = [
|
||||
{ key: 'NONE', label: t('doNotUseEncryption')},
|
||||
{ key: 'TLS', label: t('useTls –UsuallySelectedForPort465')},
|
||||
{ key: 'STARTTLS', label: t('useStarttls –UsuallySelectedForPort587')}
|
||||
];
|
||||
|
||||
const sesRegionOptions = [
|
||||
{ key: 'us-east-1', label: t('useast1')},
|
||||
{ key: 'us-west-2', label: t('uswest2')},
|
||||
{ key: 'eu-west-1', label: t('euwest1')}
|
||||
];
|
||||
|
||||
const zoneMtaTypeOptions = [];
|
||||
|
||||
if (mailtrainConfig.builtinZoneMTAEnabled) {
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.BUILTIN, label: t('builtinZoneMta')});
|
||||
}
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_HTTP_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt-1')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.REGULAR, label: t('noDynamicConfigurationOfDkimKeys')});
|
||||
|
||||
mailerTypes[MailerType.GENERIC_SMTP] = {
|
||||
typeName: typeNames[MailerType.GENERIC_SMTP],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP()
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveGenericSMTP(data);
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.ZONE_MTA] = {
|
||||
typeName: typeNames[MailerType.ZONE_MTA],
|
||||
getForm: owner => {
|
||||
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
|
||||
return (
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<Dropdown id="zoneMtaType" label={t('dynamicConfiguration')} options={zoneMtaTypeOptions}/>
|
||||
{(zoneMtaType === ZoneMTAType.REGULAR || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<div>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
{(zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<Fieldset label={t('dkimSigning')}>
|
||||
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
|
||||
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
|
||||
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
|
||||
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
|
||||
}
|
||||
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
|
||||
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
|
||||
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
|
||||
</Fieldset>
|
||||
}
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP(),
|
||||
zoneMtaType: mailtrainConfig.builtinZoneMTAEnabled ? ZoneMTAType.BUILTIN : ZoneMTAType.REGULAR,
|
||||
dkimApiKey: '',
|
||||
dkimDomain: '',
|
||||
dkimSelector: '',
|
||||
dkimPrivateKey: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
data.zoneMtaType = data.mailer_settings.zoneMtaType;
|
||||
data.dkimApiKey = data.mailer_settings.dkimApiKey;
|
||||
data.dkimDomain = data.mailer_settings.dkimDomain;
|
||||
data.dkimSelector = data.mailer_settings.dkimSelector;
|
||||
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
|
||||
},
|
||||
beforeSave: data => {
|
||||
const zoneMtaType = Number.parseInt(data.zoneMtaType);
|
||||
|
||||
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
|
||||
|
||||
data.mailer_settings.zoneMtaType = zoneMtaType;
|
||||
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
|
||||
data.mailer_settings.dkimDomain = data.dkimDomain;
|
||||
data.mailer_settings.dkimSelector = data.dkimSelector;
|
||||
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
|
||||
}
|
||||
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
|
||||
data.mailer_settings.dkimApiKey = data.dkimApiKey;
|
||||
}
|
||||
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.AWS_SES] = {
|
||||
typeName: typeNames[MailerType.AWS_SES],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
|
||||
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
|
||||
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitCommon(),
|
||||
sesKey: '',
|
||||
sesSecret: '',
|
||||
sesRegion: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadCommon(data);
|
||||
data.sesKey = data.mailer_settings.key;
|
||||
data.sesSecret = data.mailer_settings.secret;
|
||||
data.sesRegion = data.mailer_settings.region;
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveCommon(data);
|
||||
data.mailer_settings.key = data.sesKey;
|
||||
data.mailer_settings.secret = data.sesSecret;
|
||||
data.mailer_settings.region = data.sesRegion;
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.AWS_SES);
|
||||
},
|
||||
validate: state => {
|
||||
validateCommon(state);
|
||||
}
|
||||
};
|
||||
|
||||
return mailerTypes;
|
||||
}
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
|
||||
import {
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
InputField,
|
||||
TextArea
|
||||
} from "../lib/form";
|
||||
import {Trans} from "react-i18next";
|
||||
import styles from "./styles.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
export const mailerTypesOrder = [
|
||||
MailerType.ZONE_MTA,
|
||||
MailerType.GENERIC_SMTP,
|
||||
MailerType.AWS_SES
|
||||
];
|
||||
|
||||
export function getMailerTypes(t) {
|
||||
const mailerTypes = {};
|
||||
|
||||
function initFieldsIfMissing(mutStateData, mailerType) {
|
||||
const initVals = mailerTypes[mailerType].initData();
|
||||
|
||||
for (const key in initVals) {
|
||||
if (!mutStateData.hasIn([key])) {
|
||||
mutStateData.setIn([key, 'value'], initVals[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeSave(data) {
|
||||
for (const mailerKey in mailerTypes) {
|
||||
const initVals = mailerTypes[mailerKey].initData();
|
||||
for (const fieldKey in initVals) {
|
||||
delete data[fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumber(state, field, label, emptyAllowed = false) {
|
||||
const value = state.getIn([field, 'value']);
|
||||
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
|
||||
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
|
||||
} else {
|
||||
state.setIn([field, 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
function getInitCommon() {
|
||||
return {
|
||||
maxConnections: '5',
|
||||
throttling: '',
|
||||
logTransactions: false
|
||||
};
|
||||
}
|
||||
|
||||
function getInitGenericSMTP() {
|
||||
return {
|
||||
...getInitCommon(),
|
||||
smtpHostname: '',
|
||||
smtpPort: '',
|
||||
smtpEncryption: 'NONE',
|
||||
smtpUseAuth: false,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpAllowSelfSigned: false,
|
||||
smtpMaxMessages: '100'
|
||||
};
|
||||
}
|
||||
|
||||
function afterLoadCommon(data) {
|
||||
data.maxConnections = data.mailer_settings.maxConnections;
|
||||
data.throttling = data.mailer_settings.throttling || '';
|
||||
data.logTransactions = data.mailer_settings.logTransactions;
|
||||
}
|
||||
|
||||
function afterLoadGenericSMTP(data) {
|
||||
afterLoadCommon(data);
|
||||
data.smtpHostname = data.mailer_settings.hostname;
|
||||
data.smtpPort = data.mailer_settings.port || '';
|
||||
data.smtpEncryption = data.mailer_settings.encryption;
|
||||
data.smtpUseAuth = data.mailer_settings.useAuth;
|
||||
data.smtpUser = data.mailer_settings.user;
|
||||
data.smtpPassword = data.mailer_settings.password;
|
||||
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
|
||||
data.smtpMaxMessages = data.mailer_settings.maxMessages;
|
||||
}
|
||||
|
||||
function beforeSaveCommon(data) {
|
||||
data.mailer_settings = {};
|
||||
data.mailer_settings.maxConnections = Number(data.maxConnections);
|
||||
data.mailer_settings.throttling = Number(data.throttling);
|
||||
data.mailer_settings.logTransactions = data.logTransactions;
|
||||
}
|
||||
|
||||
function beforeSaveGenericSMTP(data, builtin = false) {
|
||||
beforeSaveCommon(data);
|
||||
|
||||
if (!builtin) {
|
||||
data.mailer_settings.hostname = data.smtpHostname;
|
||||
data.mailer_settings.port = Number(data.smtpPort);
|
||||
data.mailer_settings.encryption = data.smtpEncryption;
|
||||
data.mailer_settings.useAuth = data.smtpUseAuth;
|
||||
data.mailer_settings.user = data.smtpUser;
|
||||
data.mailer_settings.password = data.smtpPassword;
|
||||
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
|
||||
}
|
||||
|
||||
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
|
||||
}
|
||||
|
||||
function validateCommon(state) {
|
||||
validateNumber(state, 'maxConnections', 'Max connections');
|
||||
validateNumber(state, 'throttling', 'Throttling', true);
|
||||
}
|
||||
|
||||
function validateGenericSMTP(state) {
|
||||
validateCommon(state);
|
||||
validateNumber(state, 'smtpPort', 'Port', true);
|
||||
validateNumber(state, 'smtpMaxMessages', 'Max messages');
|
||||
}
|
||||
|
||||
const typeNames = {
|
||||
[MailerType.GENERIC_SMTP]: t('genericSmtp'),
|
||||
[MailerType.ZONE_MTA]: t('zoneMta'),
|
||||
[MailerType.AWS_SES]: t('amazonSes')
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ key: MailerType.GENERIC_SMTP, label: typeNames[MailerType.GENERIC_SMTP]},
|
||||
{ key: MailerType.ZONE_MTA, label: typeNames[MailerType.ZONE_MTA]},
|
||||
{ key: MailerType.AWS_SES, label: typeNames[MailerType.AWS_SES]}
|
||||
];
|
||||
|
||||
const smtpEncryptionOptions = [
|
||||
{ key: 'NONE', label: t('doNotUseEncryption')},
|
||||
{ key: 'TLS', label: t('useTls –UsuallySelectedForPort465')},
|
||||
{ key: 'STARTTLS', label: t('useStarttls –UsuallySelectedForPort587')}
|
||||
];
|
||||
|
||||
const sesRegionOptions = [
|
||||
{ key: 'us-east-1', label: t('useast1')},
|
||||
{ key: 'us-west-2', label: t('uswest2')},
|
||||
{ key: 'eu-west-1', label: t('euwest1')}
|
||||
];
|
||||
|
||||
const zoneMtaTypeOptions = [];
|
||||
|
||||
if (mailtrainConfig.builtinZoneMTAEnabled) {
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.BUILTIN, label: t('builtinZoneMta')});
|
||||
}
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_HTTP_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt-1')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.REGULAR, label: t('noDynamicConfigurationOfDkimKeys')});
|
||||
|
||||
mailerTypes[MailerType.GENERIC_SMTP] = {
|
||||
typeName: typeNames[MailerType.GENERIC_SMTP],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP()
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveGenericSMTP(data);
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.ZONE_MTA] = {
|
||||
typeName: typeNames[MailerType.ZONE_MTA],
|
||||
getForm: owner => {
|
||||
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
|
||||
return (
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<Dropdown id="zoneMtaType" label={t('dynamicConfiguration')} options={zoneMtaTypeOptions}/>
|
||||
{(zoneMtaType === ZoneMTAType.REGULAR || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<div>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
{(zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<Fieldset label={t('dkimSigning')}>
|
||||
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
|
||||
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
|
||||
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
|
||||
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
|
||||
}
|
||||
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
|
||||
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
|
||||
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
|
||||
</Fieldset>
|
||||
}
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP(),
|
||||
zoneMtaType: mailtrainConfig.builtinZoneMTAEnabled ? ZoneMTAType.BUILTIN : ZoneMTAType.REGULAR,
|
||||
dkimApiKey: '',
|
||||
dkimDomain: '',
|
||||
dkimSelector: '',
|
||||
dkimPrivateKey: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
data.zoneMtaType = data.mailer_settings.zoneMtaType;
|
||||
data.dkimApiKey = data.mailer_settings.dkimApiKey;
|
||||
data.dkimDomain = data.mailer_settings.dkimDomain;
|
||||
data.dkimSelector = data.mailer_settings.dkimSelector;
|
||||
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
|
||||
},
|
||||
beforeSave: data => {
|
||||
const zoneMtaType = Number.parseInt(data.zoneMtaType);
|
||||
|
||||
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
|
||||
|
||||
data.mailer_settings.zoneMtaType = zoneMtaType;
|
||||
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
|
||||
data.mailer_settings.dkimDomain = data.dkimDomain;
|
||||
data.mailer_settings.dkimSelector = data.dkimSelector;
|
||||
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
|
||||
}
|
||||
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
|
||||
data.mailer_settings.dkimApiKey = data.dkimApiKey;
|
||||
}
|
||||
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.AWS_SES] = {
|
||||
typeName: typeNames[MailerType.AWS_SES],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
|
||||
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
|
||||
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitCommon(),
|
||||
sesKey: '',
|
||||
sesSecret: '',
|
||||
sesRegion: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadCommon(data);
|
||||
data.sesKey = data.mailer_settings.key;
|
||||
data.sesSecret = data.mailer_settings.secret;
|
||||
data.sesRegion = data.mailer_settings.region;
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveCommon(data);
|
||||
data.mailer_settings.key = data.sesKey;
|
||||
data.mailer_settings.secret = data.sesSecret;
|
||||
data.mailer_settings.region = data.sesRegion;
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.AWS_SES);
|
||||
},
|
||||
validate: state => {
|
||||
validateCommon(state);
|
||||
}
|
||||
};
|
||||
|
||||
return mailerTypes;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
textarea.dkimPrivateKey {
|
||||
height: 200px;
|
||||
textarea.dkimPrivateKey {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.overridableCheckbox {
|
||||
margin-top: -8px !important;
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,51 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {ACEEditor} from "../../lib/form";
|
||||
import 'brace/mode/html'
|
||||
import 'brace/mode/xml'
|
||||
|
||||
export function getTemplateTypesOrder() {
|
||||
return [/* 'mjml' , */ 'html'];
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
const templateTypes = {};
|
||||
|
||||
function clearBeforeSend(data) {
|
||||
delete data.html;
|
||||
delete data.mjml;
|
||||
}
|
||||
|
||||
templateTypes.html = {
|
||||
typeName: t('html'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.html = data.data.html;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
html: data.html
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
templateTypes.mjml = {
|
||||
typeName: t('mjml'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.mjml = data.data.mjml;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
mjml: data.mjml
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
return templateTypes;
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {ACEEditor} from "../../lib/form";
|
||||
import 'brace/mode/html'
|
||||
import 'brace/mode/xml'
|
||||
|
||||
export function getTemplateTypesOrder() {
|
||||
return [/* 'mjml' , */ 'html'];
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
const templateTypes = {};
|
||||
|
||||
function clearBeforeSend(data) {
|
||||
delete data.html;
|
||||
delete data.mjml;
|
||||
}
|
||||
|
||||
templateTypes.html = {
|
||||
typeName: t('html'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.html = data.data.html;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
html: data.html
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
templateTypes.mjml = {
|
||||
typeName: t('mjml'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.mjml = data.data.mjml;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
mjml: data.mjml
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
return templateTypes;
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
The file icons-rtl.gif is assumed by skin-common.less. However skin-bootstrap does not come with icons.
|
||||
The file icons-rtl.gif is assumed by skin-common.less. However skin-bootstrap does not come with icons.
|
||||
To avoid errors with webpack an empty file has been provided.
|
4
client/static/mosaico/uploads/.gitignore
vendored
4
client/static/mosaico/uploads/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
|
@ -1,7 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./noto-sans-400-normal.eot');
|
||||
src: local('Noto Sans'), local('NotoSans'), url('./noto-sans-400-normal.eot#iefix') format('embedded-opentype'), url('./noto-sans-400-normal.woff') format('woff'), url('./noto-sans-400-normal.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./noto-sans-400-normal.eot');
|
||||
src: local('Noto Sans'), local('NotoSans'), url('./noto-sans-400-normal.eot#iefix') format('embedded-opentype'), url('./noto-sans-400-normal.woff') format('woff'), url('./noto-sans-400-normal.ttf') format('truetype');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue