WiP on segments
This commit is contained in:
parent
6cc34136f5
commit
f3ff89c536
21 changed files with 945 additions and 352 deletions
|
@ -27,6 +27,7 @@
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^15.6.1",
|
"react": "^15.6.1",
|
||||||
"react-ace": "^5.1.0",
|
"react-ace": "^5.1.0",
|
||||||
|
"react-day-picker": "^6.1.0",
|
||||||
"react-dom": "^15.6.1",
|
"react-dom": "^15.6.1",
|
||||||
"react-i18next": "^4.6.1",
|
"react-i18next": "^4.6.1",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
|
|
|
@ -19,6 +19,14 @@ import 'brace/mode/json';
|
||||||
import 'brace/mode/handlebars';
|
import 'brace/mode/handlebars';
|
||||||
import 'brace/theme/github';
|
import 'brace/theme/github';
|
||||||
|
|
||||||
|
import DayPicker from 'react-day-picker';
|
||||||
|
import 'react-day-picker/lib/style.css';
|
||||||
|
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../shared/date';
|
||||||
|
|
||||||
|
import styles from "./styles.scss";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
|
||||||
const FormState = {
|
const FormState = {
|
||||||
Loading: 0,
|
Loading: 0,
|
||||||
LoadingWithNotice: 1,
|
LoadingWithNotice: 1,
|
||||||
|
@ -79,7 +87,7 @@ class Form extends Component {
|
||||||
|
|
||||||
if (!owner.isFormReady()) {
|
if (!owner.isFormReady()) {
|
||||||
if (owner.isFormWithLoadingNotice()) {
|
if (owner.isFormWithLoadingNotice()) {
|
||||||
return <p className={`alert alert-info mt-form-status`} role="alert">{t('Loading ...')}</p>
|
return <p className={`alert alert-info ${styles.formStatus}`} role="alert">{t('Loading ...')}</p>
|
||||||
} else {
|
} else {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
@ -91,7 +99,7 @@ class Form extends Component {
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{statusMessageText &&
|
{statusMessageText &&
|
||||||
<AlignedRow htmlId="form-status-message">
|
<AlignedRow htmlId="form-status-message">
|
||||||
<p className={`alert alert-${statusMessageSeverity} mt-form-status`} role="alert">{statusMessageText}</p>
|
<p className={`alert alert-${statusMessageSeverity} ${styles.formStatus}`} role="alert">{statusMessageText}</p>
|
||||||
</AlignedRow>
|
</AlignedRow>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
|
@ -295,6 +303,111 @@ class TextArea extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DatePicker extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
opened: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||||
|
format: PropTypes.string,
|
||||||
|
birthday: PropTypes.bool,
|
||||||
|
dateFormat: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
dateFormat: DateFormat.INTL
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
formStateOwner: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDayPicker() {
|
||||||
|
this.setState({
|
||||||
|
opened: !this.state.opened
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
daySelected(date) {
|
||||||
|
const owner = this.context.formStateOwner;
|
||||||
|
const id = this.props.id;
|
||||||
|
const props = this.props;
|
||||||
|
owner.updateFormValue(id, props.birthday ? formatBirthday(props.dateFormat, date) : formatDate(props.dateFormat, date));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
opened: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
const owner = this.context.formStateOwner;
|
||||||
|
const id = this.props.id;
|
||||||
|
const htmlId = 'form_' + id;
|
||||||
|
|
||||||
|
function BirthdayPickerCaption({ date, localeUtils, onChange }) {
|
||||||
|
const months = localeUtils.getMonths();
|
||||||
|
return (
|
||||||
|
<div className="DayPicker-Caption">
|
||||||
|
{months[date.getMonth()]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedDate, captionElement, fromMonth, toMonth, placeholder;
|
||||||
|
const selectedDateStr = owner.getFormValue(id) || '';
|
||||||
|
if (props.birthday) {
|
||||||
|
selectedDate = parseBirthday(props.dateFormat, selectedDateStr);
|
||||||
|
if (!selectedDate) {
|
||||||
|
selectedDate = moment().set('year', birthdayYear).toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
captionElement = <BirthdayPickerCaption/>;
|
||||||
|
fromMonth = new Date(birthdayYear, 0, 1);
|
||||||
|
toMonth = new Date(birthdayYear, 11, 31);
|
||||||
|
placeholder = getBirthdayFormatString(props.dateFormat);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
selectedDate = parseDate(props.dateFormat, selectedDateStr);
|
||||||
|
if (!selectedDate) {
|
||||||
|
selectedDate = moment().toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholder = getDateFormatString(props.dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||||
|
<div>
|
||||||
|
<div className="input-group">
|
||||||
|
<input type="text" value={selectedDateStr} placeholder={placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
|
||||||
|
<span className="input-group-addon" onClick={::this.toggleDayPicker}><span className="glyphicon glyphicon-th"></span></span>
|
||||||
|
</div>
|
||||||
|
{this.state.opened &&
|
||||||
|
<div className={styles.dayPickerWrapper}>
|
||||||
|
<DayPicker
|
||||||
|
onDayClick={date => this.daySelected(date)}
|
||||||
|
selectedDays={selectedDate}
|
||||||
|
initialMonth={selectedDate}
|
||||||
|
fromMonth={fromMonth}
|
||||||
|
toMonth={toMonth}
|
||||||
|
captionElement={captionElement}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Dropdown extends Component {
|
class Dropdown extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
@ -374,7 +487,7 @@ class ButtonRow extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let className = 'mt-button-row';
|
let className = styles.buttonRow;
|
||||||
if (this.props.className) {
|
if (this.props.className) {
|
||||||
className += ' ' + this.props.className;
|
className += ' ' + this.props.className;
|
||||||
}
|
}
|
||||||
|
@ -554,13 +667,13 @@ class TableSelect extends Component {
|
||||||
if (props.dropdown) {
|
if (props.dropdown) {
|
||||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||||
<div>
|
<div>
|
||||||
<div className="input-group mt-tableselect-dropdown">
|
<div className={`input-group ${styles.tableSelectDropdown}`}>
|
||||||
<input type="text" className="form-control" value={this.state.selectedLabel} readOnly onClick={::this.toggleOpen}/>
|
<input type="text" className="form-control" value={this.state.selectedLabel} readOnly onClick={::this.toggleOpen}/>
|
||||||
<span className="input-group-btn">
|
<span className="input-group-btn">
|
||||||
<ActionButton label={t('Select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
|
<ActionButton label={t('Select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-tableselect-table' + (this.state.open ? '' : ' mt-tableselect-table-hidden')}>
|
<div className={styles.tableSelectTable + (this.state.open ? '' : ' ' + styles.tableSelectTableHidden)}>
|
||||||
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -834,13 +947,29 @@ function withForm(target) {
|
||||||
this.setState(previousState => {
|
this.setState(previousState => {
|
||||||
const oldValue = previousState.formState.getIn(['data', key, 'value']);
|
const oldValue = previousState.formState.getIn(['data', key, 'value']);
|
||||||
|
|
||||||
|
const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {};
|
||||||
|
|
||||||
|
const formState = previousState.formState.withMutations(mutState => {
|
||||||
|
mutState.update('data', stateData => stateData.withMutations(mutStateData => {
|
||||||
|
if (typeof onChangeBeforeValidationCallback === 'object') {
|
||||||
|
if (onChangeBeforeValidationCallback[key]) {
|
||||||
|
onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onChangeBeforeValidationCallback(mutStateData, key, oldValue, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutStateData.setIn([key, 'value'], value);
|
||||||
|
}));
|
||||||
|
|
||||||
|
validateFormState(this, mutState);
|
||||||
|
});
|
||||||
|
|
||||||
let newState = {
|
let newState = {
|
||||||
formState: previousState.formState.withMutations(mutState => {
|
formState
|
||||||
mutState.setIn(['data', key, 'value'], value);
|
|
||||||
validateFormState(this, mutState);
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onChangeCallback = this.state.formSettings.onChange || {};
|
const onChangeCallback = this.state.formSettings.onChange || {};
|
||||||
|
|
||||||
if (typeof onChangeCallback === 'object') {
|
if (typeof onChangeCallback === 'object') {
|
||||||
|
@ -1000,6 +1129,7 @@ export {
|
||||||
InputField,
|
InputField,
|
||||||
CheckBox,
|
CheckBox,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
DatePicker,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
AlignedRow,
|
AlignedRow,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
.mt-button-row > * {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-button-row > *:last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-form-status {
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-links > * {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-links > *:last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-horizontal .control-label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-form-disabled {
|
|
||||||
background-color: #eeeeee;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ace_editor {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-tableselect-dropdown {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-tableselect-table.mt-tableselect-table-hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
height: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-tableselect-dropdown input[readonly] {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3.legend {
|
|
||||||
font-size: 21px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-secondary-nav {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.mt-secondary-nav {
|
|
||||||
margin: 0px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 5px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-secondary-nav > li {
|
|
||||||
display: inline-block;
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-secondary-nav > li > a {
|
|
||||||
padding: 3px 10px;
|
|
||||||
}
|
|
|
@ -5,12 +5,12 @@ import { translate } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom'
|
import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom'
|
||||||
import './page.css';
|
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
import { DismissibleAlert, Button } from './bootstrap-components';
|
import { DismissibleAlert, Button } from './bootstrap-components';
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
|
import styles from "./styles.scss";
|
||||||
|
|
||||||
|
|
||||||
class Breadcrumb extends Component {
|
class Breadcrumb extends Component {
|
||||||
|
@ -133,7 +133,7 @@ class SecondaryNavBar extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderedElems.length > 1) {
|
if (renderedElems.length > 1) {
|
||||||
let className = 'mt-secondary-nav nav nav-pills';
|
let className = styles.secondaryNav + ' nav nav-pills';
|
||||||
if (this.props.className) {
|
if (this.props.className) {
|
||||||
className += ' ' + this.props.className;
|
className += ' ' + this.props.className;
|
||||||
}
|
}
|
||||||
|
@ -482,7 +482,7 @@ class Toolbar extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let className = 'pull-right mt-button-row';
|
let className = 'pull-right ' + styles.buttonRow;
|
||||||
if (this.props.className) {
|
if (this.props.className) {
|
||||||
className += ' ' + this.props.className;
|
className += ' ' + this.props.className;
|
||||||
}
|
}
|
||||||
|
|
89
client/src/lib/styles.scss
Normal file
89
client/src/lib/styles.scss
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
:global .DayPicker {
|
||||||
|
border-left: 1px solid lightgray;
|
||||||
|
border-right: 1px solid lightgray;
|
||||||
|
border-bottom: 1px solid lightgray;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayPickerWrapper {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.buttonRow > * {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonRow > *:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formStatus {
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionLinks > * {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionLinks > *:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .form-horizontal .control-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formDisabled {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .ace_editor {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSelectDropdown {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSelectTable.tableSelectTableHidden {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSelectDropdown input[readonly] {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global h3.legend {
|
||||||
|
font-size: 21px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryNav {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.secondaryNav {
|
||||||
|
margin: 0px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 5px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryNav > li {
|
||||||
|
display: inline-block;
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryNav > li > a {
|
||||||
|
padding: 3px 10px;
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import axios from './axios';
|
||||||
|
|
||||||
import { withPageHelpers } from '../lib/page'
|
import { withPageHelpers } from '../lib/page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
|
import styles from "./styles.scss";
|
||||||
|
|
||||||
//dtFactory();
|
//dtFactory();
|
||||||
//dtSelectFactory();
|
//dtSelectFactory();
|
||||||
|
@ -169,7 +170,7 @@ class Table extends Component {
|
||||||
|
|
||||||
this.selectionMap = nextSelectionMap;
|
this.selectionMap = nextSelectionMap;
|
||||||
|
|
||||||
return updateDueToSelectionChange || this.props.data != nextProps.data || this.props.dataUrl != nextProps.dataUrl;
|
return updateDueToSelectionChange || this.props.data !== nextProps.data || this.props.dataUrl !== nextProps.dataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -179,7 +180,7 @@ class Table extends Component {
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
if (column.actions) {
|
if (column.actions) {
|
||||||
const createdCellFn = (td, data, rowData) => {
|
const createdCellFn = (td, data, rowData) => {
|
||||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
const linksContainer = jQuery(`<span class="${styles.actionLinks}"/>`);
|
||||||
|
|
||||||
let actions = column.actions(rowData);
|
let actions = column.actions(rowData);
|
||||||
let options = {};
|
let options = {};
|
||||||
|
@ -322,19 +323,20 @@ class Table extends Component {
|
||||||
if (this.props.data) {
|
if (this.props.data) {
|
||||||
this.table.clear();
|
this.table.clear();
|
||||||
this.table.rows.add(this.props.data);
|
this.table.rows.add(this.props.data);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const self = this;
|
this.refresh();
|
||||||
this.table.rows().every(function() {
|
|
||||||
const key = this.data()[self.props.selectionKeyIndex];
|
|
||||||
if (self.selectionMap.has(key)) {
|
|
||||||
jQuery(this.node()).addClass('selected');
|
|
||||||
} else {
|
|
||||||
jQuery(this.node()).removeClass('selected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
this.table.rows().every(function() {
|
||||||
|
const key = this.data()[self.props.selectionKeyIndex];
|
||||||
|
if (self.selectionMap.has(key)) {
|
||||||
|
jQuery(this.node()).addClass('selected');
|
||||||
|
} else {
|
||||||
|
jQuery(this.node()).removeClass('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.updateSelectInfo();
|
this.updateSelectInfo();
|
||||||
this.fetchAndNotifySelectionData();
|
this.fetchAndNotifySelectionData();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import axios from './axios';
|
||||||
|
|
||||||
import { withPageHelpers } from '../lib/page'
|
import { withPageHelpers } from '../lib/page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
|
import styles from "./styles.scss";
|
||||||
|
|
||||||
const TreeSelectMode = {
|
const TreeSelectMode = {
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
|
@ -122,7 +123,7 @@ class TreeTable extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.actions) {
|
if (this.props.actions) {
|
||||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
const linksContainer = jQuery(`<span class="${styles.actionLinks}"/>`);
|
||||||
|
|
||||||
const actions = this.props.actions(node);
|
const actions = this.props.actions(node);
|
||||||
for (const {label, link} of actions) {
|
for (const {label, link} of actions) {
|
||||||
|
|
|
@ -14,7 +14,8 @@ import { getFieldTypes } from './field-types';
|
||||||
import interoperableErrors from '../../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../../shared/interoperable-errors';
|
||||||
import validators from '../../../../shared/validators';
|
import validators from '../../../../shared/validators';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
import { parseDate, parseBirthday } from '../../../../shared/fields';
|
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
|
||||||
|
import styles from "../../lib/styles.scss";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -72,7 +73,7 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
data.enumOptions = '';
|
data.enumOptions = '';
|
||||||
data.dateFormat = 'eur';
|
data.dateFormat = DateFormat.EUR;
|
||||||
data.renderTemplate = '';
|
data.renderTemplate = '';
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
@ -374,8 +375,8 @@ export default class CUD extends Component {
|
||||||
<Fieldset label={t('Field settings')}>
|
<Fieldset label={t('Field settings')}>
|
||||||
<Dropdown id="dateFormat" label={t('Date format')}
|
<Dropdown id="dateFormat" label={t('Date format')}
|
||||||
options={[
|
options={[
|
||||||
{key: 'us', label: t('MM/DD/YYYY')},
|
{key: DateFormat.US, label: t('MM/DD/YYYY')},
|
||||||
{key: 'eur', label: t('DD/MM/YYYY')}
|
{key: DateFormat.EU, label: t('DD/MM/YYYY')}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
|
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
|
||||||
|
@ -387,8 +388,8 @@ export default class CUD extends Component {
|
||||||
<Fieldset label={t('Field settings')}>
|
<Fieldset label={t('Field settings')}>
|
||||||
<Dropdown id="dateFormat" label={t('Date format')}
|
<Dropdown id="dateFormat" label={t('Date format')}
|
||||||
options={[
|
options={[
|
||||||
{key: 'us', label: t('MM/DD')},
|
{key: DateFormat.US, label: t('MM/DD')},
|
||||||
{key: 'eur', label: t('DD/MM')}
|
{key: DateFormat.EU, label: t('DD/MM')}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
|
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
|
||||||
|
@ -445,7 +446,7 @@ export default class CUD extends Component {
|
||||||
<InputField id="name" label={t('Name')}/>
|
<InputField id="name" label={t('Name')}/>
|
||||||
|
|
||||||
{isEdit ?
|
{isEdit ?
|
||||||
<StaticField id="type" className="mt-form-disabled" label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
|
<StaticField id="type" className={styles.formDisabled} label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
|
||||||
:
|
:
|
||||||
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
|
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,13 @@ const getStructure = t => {
|
||||||
navs: {
|
navs: {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
title: t('Subscribers'),
|
title: t('Subscribers'),
|
||||||
|
resolve: {
|
||||||
|
segments: params => `/rest/segments/${params.listId}`
|
||||||
|
},
|
||||||
|
extraParams: [':segmentId?'],
|
||||||
link: params => `/lists/${params.listId}/subscriptions`,
|
link: params => `/lists/${params.listId}/subscriptions`,
|
||||||
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
|
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
|
||||||
render: props => <SubscriptionsList list={props.resolved.list} />
|
render: props => <SubscriptionsList list={props.resolved.list} segments={props.resolved.segments} segmentId={props.match.params.segmentId} />
|
||||||
},
|
},
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
|
@ -163,7 +167,7 @@ const getStructure = t => {
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nextProvider i18n={ i18n }><Section root='/lists/1/segments/create' /* FIXME */ structure={getStructure}/></I18nextProvider>,
|
<I18nextProvider i18n={ i18n }><Section root='/lists' structure={getStructure}/></I18nextProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -93,6 +93,10 @@ export default class CUD extends Component {
|
||||||
await this.getFormValuesFromURL(`/rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
|
await this.getFormValuesFromURL(`/rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
|
||||||
data.rootRuleType = data.settings.rootRule.type;
|
data.rootRuleType = data.settings.rootRule.type;
|
||||||
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
|
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
rulesTree: this.getTreeFromRules(data.settings.rootRule.rules)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default class CUD extends Component {
|
||||||
this.state = {};
|
this.state = {};
|
||||||
|
|
||||||
this.initForm({
|
this.initForm({
|
||||||
|
onChangeBeforeValidation: ::this.populateRuleDefaults,
|
||||||
onChange: ::this.onFormChange
|
onChange: ::this.onFormChange
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -45,12 +46,12 @@ export default class CUD extends Component {
|
||||||
const ruleHelpers = this.ruleHelpers;
|
const ruleHelpers = this.ruleHelpers;
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
if (!ruleHelpers.isCompositeRuleType(rule.type)) {
|
if (!ruleHelpers.isCompositeRuleType(rule.type)) { // rule.type === null signifies primitive rule where the type has not been determined yet
|
||||||
|
data = ruleHelpers.primitiveRuleTypesFormDataDefaults;
|
||||||
|
|
||||||
const settings = ruleHelpers.getRuleTypeSettings(rule);
|
const settings = ruleHelpers.getRuleTypeSettings(rule);
|
||||||
if (settings) {
|
if (settings) {
|
||||||
data = settings.getFormData(rule);
|
Object.assign(data, settings.getFormData(rule));
|
||||||
} else {
|
|
||||||
data = {}; // This handles the case of a new rule, which does not have a type and column yet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.type = rule.type || ''; // On '', we display label "--SELECT--" in the type dropdown. Null would not be accepted by React.
|
data.type = rule.type || ''; // On '', we display label "--SELECT--" in the type dropdown. Null would not be accepted by React.
|
||||||
|
@ -89,10 +90,10 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
const ruleType = state.getIn(['type', 'value']);
|
const ruleType = state.getIn(['type', 'value']);
|
||||||
if (!ruleHelpers.isCompositeRuleType(ruleType)) {
|
if (!ruleHelpers.isCompositeRuleType(ruleType)) {
|
||||||
const columnType = state.getIn(['column', 'value']);
|
const column = state.getIn(['column', 'value']);
|
||||||
|
|
||||||
if (columnType) {
|
if (column) {
|
||||||
const colType = ruleHelpers.getColumnType(columnType);
|
const colType = ruleHelpers.getColumnType(column);
|
||||||
|
|
||||||
if (ruleType) {
|
if (ruleType) {
|
||||||
const settings = ruleHelpers.primitiveRuleTypes[colType][ruleType];
|
const settings = ruleHelpers.primitiveRuleTypes[colType][ruleType];
|
||||||
|
@ -106,6 +107,27 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
populateRuleDefaults(mutState) {
|
||||||
|
const ruleHelpers = this.ruleHelpers;
|
||||||
|
const type = mutState.getIn(['data','type','value']);
|
||||||
|
|
||||||
|
if (!ruleHelpers.isCompositeRuleType(type)) {
|
||||||
|
const column = mutState.getIn(['data', 'column', 'value']);
|
||||||
|
|
||||||
|
if (column) {
|
||||||
|
const colType = ruleHelpers.getColumnType(column);
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
const settings = ruleHelpers.primitiveRuleTypes[colType][type];
|
||||||
|
if (!settings) {
|
||||||
|
// The existing rule type does not fit the newly changed column. This resets the rule type chooser to "-- Select ---"
|
||||||
|
mutState.setIn(['data', 'type', 'value'], '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onFormChange(newState) {
|
onFormChange(newState) {
|
||||||
const noErrors = !newState.formState.get('data').find(attr => attr.get('error'));
|
const noErrors = !newState.formState.get('data').find(attr => attr.get('error'));
|
||||||
|
|
||||||
|
@ -136,7 +158,6 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const rule = this.props.rule;
|
const rule = this.props.rule;
|
||||||
|
@ -153,7 +174,7 @@ export default class CUD extends Component {
|
||||||
{ data: 3, title: t('Merge Tag') }
|
{ data: 3, title: t('Merge Tag') }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ruleColumnOptions = ruleHelpers.fields.map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type].label, fld.tag || '' ]);
|
const ruleColumnOptions = ruleHelpers.fields.map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type].label, fld.key || '' ]);
|
||||||
|
|
||||||
const ruleColumnSelect = <TableSelect id="column" label={t('Field')} data={ruleColumnOptions} columns={ruleColumnOptionsColumns} dropdown withHeader selectionLabelIndex={1} />;
|
const ruleColumnSelect = <TableSelect id="column" label={t('Field')} data={ruleColumnOptions} columns={ruleColumnOptionsColumns} dropdown withHeader selectionLabelIndex={1} />;
|
||||||
let ruleTypeSelect = null;
|
let ruleTypeSelect = null;
|
||||||
|
@ -186,6 +207,7 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.ruleOptions}>
|
<div className={styles.ruleOptions}>
|
||||||
<h3>{t('Rule Options')}</h3>
|
<h3>{t('Rule Options')}</h3>
|
||||||
|
@ -198,6 +220,7 @@ export default class CUD extends Component {
|
||||||
<Button type="submit" className="btn-primary" icon="chevron-left" label={t('OK')}/>
|
<Button type="submit" className="btn-primary" icon="chevron-left" label={t('OK')}/>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {InputField} from "../../lib/form";
|
import {DatePicker, Dropdown, InputField} from "../../lib/form";
|
||||||
|
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
|
||||||
|
|
||||||
|
|
||||||
export function getRuleHelpers(t, fields) {
|
export function getRuleHelpers(t, fields) {
|
||||||
|
|
||||||
function formatDate(date) {
|
|
||||||
return date; // FIXME
|
|
||||||
}
|
|
||||||
|
|
||||||
const ruleHelpers = {};
|
const ruleHelpers = {};
|
||||||
|
|
||||||
ruleHelpers.compositeRuleTypes = {
|
ruleHelpers.compositeRuleTypes = {
|
||||||
|
@ -108,49 +106,72 @@ export function getRuleHelpers(t, fields) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleHelpers.primitiveRuleTypes.date = ruleHelpers.primitiveRuleTypes.birthday = {
|
ruleHelpers.primitiveRuleTypes.date = {
|
||||||
eq: {
|
eq: {
|
||||||
dropdownLabel: t('On'),
|
dropdownLabel: t('On'),
|
||||||
treeLabel: rule => t('Date in column "{{colName}}" is {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
|
treeLabel: rule => t('Date in column "{{colName}}" is {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||||
},
|
},
|
||||||
lt: {
|
lt: {
|
||||||
dropdownLabel: t('Before'),
|
dropdownLabel: t('Before'),
|
||||||
treeLabel: rule => t('Date in column "{{colName}}" is before {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
|
treeLabel: rule => t('Date in column "{{colName}}" is before {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||||
},
|
},
|
||||||
le: {
|
le: {
|
||||||
dropdownLabel: t('Before or on'),
|
dropdownLabel: t('Before or on'),
|
||||||
treeLabel: rule => t('Date in column "{{colName}}" is before or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
|
treeLabel: rule => t('Date in column "{{colName}}" is before or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||||
},
|
},
|
||||||
gt: {
|
gt: {
|
||||||
dropdownLabel: t('After'),
|
dropdownLabel: t('After'),
|
||||||
treeLabel: rule => t('Date in column "{{colName}}" is after {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
|
treeLabel: rule => t('Date in column "{{colName}}" is after {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||||
},
|
},
|
||||||
ge: {
|
ge: {
|
||||||
dropdownLabel: t('After or on'),
|
dropdownLabel: t('After or on'),
|
||||||
treeLabel: rule => t('Date in column "{{colName}}" is after or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
|
treeLabel: rule => t('Date in column "{{colName}}" is after or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||||
},
|
},
|
||||||
eqNowPlusDays: {
|
eqTodayPlusDays: {
|
||||||
dropdownLabel: t('On x-th day before/after now'),
|
dropdownLabel: t('On x-th day before/after current date'),
|
||||||
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is'),
|
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is'),
|
||||||
},
|
},
|
||||||
ltNowPlusDays: {
|
ltTodayPlusDays: {
|
||||||
dropdownLabel: t('Before x-th day before/after now'),
|
dropdownLabel: t('Before x-th day before/after current date'),
|
||||||
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before'),
|
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before'),
|
||||||
},
|
},
|
||||||
leNowPlusDays: {
|
leTodayPlusDays: {
|
||||||
dropdownLabel: t('Before or on x-th day before/after now'),
|
dropdownLabel: t('Before or on x-th day before/after current date'),
|
||||||
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before or on'),
|
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before or on'),
|
||||||
},
|
},
|
||||||
gtNowPlusDays: {
|
gtTodayPlusDays: {
|
||||||
dropdownLabel: t('After x-th day before/after now'),
|
dropdownLabel: t('After x-th day before/after current date'),
|
||||||
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after'),
|
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after'),
|
||||||
},
|
},
|
||||||
geNowPlusDays: {
|
geTodayPlusDays: {
|
||||||
dropdownLabel: t('After or on x-th day before/after now'),
|
dropdownLabel: t('After or on x-th day before/after current date'),
|
||||||
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after or on'),
|
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after or on'),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ruleHelpers.primitiveRuleTypes.birthday = {
|
||||||
|
eq: {
|
||||||
|
dropdownLabel: t('On'),
|
||||||
|
treeLabel: rule => t('Date in column "{{colName}}" is {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||||
|
},
|
||||||
|
lt: {
|
||||||
|
dropdownLabel: t('Before'),
|
||||||
|
treeLabel: rule => t('Date in column "{{colName}}" is before {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||||
|
},
|
||||||
|
le: {
|
||||||
|
dropdownLabel: t('Before or on'),
|
||||||
|
treeLabel: rule => t('Date in column "{{colName}}" is before or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||||
|
},
|
||||||
|
gt: {
|
||||||
|
dropdownLabel: t('After'),
|
||||||
|
treeLabel: rule => t('Date in column "{{colName}}" is after {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||||
|
},
|
||||||
|
ge: {
|
||||||
|
dropdownLabel: t('After or on'),
|
||||||
|
treeLabel: rule => t('Date in column "{{colName}}" is after or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ruleHelpers.primitiveRuleTypes.option = {
|
ruleHelpers.primitiveRuleTypes.option = {
|
||||||
isTrue: {
|
isTrue: {
|
||||||
dropdownLabel: t('Is selected'),
|
dropdownLabel: t('Is selected'),
|
||||||
|
@ -194,7 +215,7 @@ export function getRuleHelpers(t, fields) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const stringValueSettings = {
|
const stringValueSettings = allowEmpty => ({
|
||||||
form: <InputField id="value" label={t('Value')} />,
|
form: <InputField id="value" label={t('Value')} />,
|
||||||
getFormData: rule => ({
|
getFormData: rule => ({
|
||||||
value: rule.value
|
value: rule.value
|
||||||
|
@ -203,13 +224,13 @@ export function getRuleHelpers(t, fields) {
|
||||||
rule.value = getter('value');
|
rule.value = getter('value');
|
||||||
},
|
},
|
||||||
validate: state => {
|
validate: state => {
|
||||||
if (!state.getIn(['value', 'value'])) {
|
if (!allowEmpty && !state.getIn(['value', 'value'])) {
|
||||||
state.setIn(['value', 'error'], t('Value must not be empty'));
|
state.setIn(['value', 'error'], t('Value must not be empty'));
|
||||||
} else {
|
} else {
|
||||||
state.setIn(['value', 'error'], null);
|
state.setIn(['value', 'error'], null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const numberValueSettings = {
|
const numberValueSettings = {
|
||||||
form: <InputField id="value" label={t('Value')} />,
|
form: <InputField id="value" label={t('Value')} />,
|
||||||
|
@ -232,23 +253,79 @@ export function getRuleHelpers(t, fields) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const birthdayValueSettings = {
|
const birthdayValueSettings = {
|
||||||
form: <InputField id="value" label={t('Value')} /> // FIXME
|
form: <DatePicker id="value" label={t('Date')} birthday />,
|
||||||
};
|
getFormData: rule => ({
|
||||||
|
value: formatBirthday(DateFormat.INTL, rule.value)
|
||||||
const birthdayRelativeValueSettings = {
|
}),
|
||||||
form: <InputField id="value" label={t('Value')} /> // FIXME
|
assignRuleSettings: (rule, getter) => {
|
||||||
|
rule.value = parseBirthday(DateFormat.INTL, getter('value')).toISOString();
|
||||||
|
},
|
||||||
|
validate: state => {
|
||||||
|
const value = state.getIn(['value', 'value']);
|
||||||
|
const date = parseBirthday(DateFormat.INTL, value);
|
||||||
|
if (!value) {
|
||||||
|
state.setIn(['value', 'error'], t('Date must not be empty'));
|
||||||
|
} else if (!date) {
|
||||||
|
state.setIn(['value', 'error'], t('Date is invalid'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['value', 'error'], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateValueSettings = {
|
const dateValueSettings = {
|
||||||
form: <InputField id="value" label={t('Value')} /> // FIXME
|
form: <DatePicker id="value" label={t('Date')} />,
|
||||||
|
getFormData: rule => ({
|
||||||
|
value: formatDate(DateFormat.INTL, rule.value)
|
||||||
|
}),
|
||||||
|
assignRuleSettings: (rule, getter) => {
|
||||||
|
rule.value = parseDate(DateFormat.INTL, getter('value')).toISOString();
|
||||||
|
},
|
||||||
|
validate: state => {
|
||||||
|
const value = state.getIn(['value', 'value']);
|
||||||
|
const date = parseDate(DateFormat.INTL, value);
|
||||||
|
if (!value) {
|
||||||
|
state.setIn(['value', 'error'], t('Date must not be empty'));
|
||||||
|
} else if (!date) {
|
||||||
|
state.setIn(['value', 'error'], t('Date is invalid'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['value', 'error'], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateRelativeValueSettings = {
|
const dateRelativeValueSettings = {
|
||||||
form: <InputField id="value" label={t('Value')} /> // FIXME
|
form:
|
||||||
|
<div>
|
||||||
|
<InputField id="value" label={t('Number of days')}/>
|
||||||
|
<Dropdown id="direction" label={t('Before/After')} options={[
|
||||||
|
{ key: 'before', label: t('Before current date') },
|
||||||
|
{ key: 'after', label: t('After current date') }
|
||||||
|
]}/>
|
||||||
|
</div>,
|
||||||
|
getFormData: rule => ({
|
||||||
|
value: Math.abs(rule.value).toString(),
|
||||||
|
direction: rule.value >= 0 ? 'after' : 'before'
|
||||||
|
}),
|
||||||
|
assignRuleSettings: (rule, getter) => {
|
||||||
|
const direction = getter('direction');
|
||||||
|
rule.value = parseInt(getter('value')) * (direction === 'before' ? -1 : 1);
|
||||||
|
},
|
||||||
|
validate: state => {
|
||||||
|
const value = state.getIn(['value', 'value']);
|
||||||
|
if (!value) {
|
||||||
|
state.setIn(['value', 'error'], t('Value must not be empty'));
|
||||||
|
} else if (isNaN(value)) {
|
||||||
|
state.setIn(['value', 'error'], t('Value must be a number'));
|
||||||
|
} else {
|
||||||
|
state.setIn(['value', 'error'], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const optionValueSettings = {
|
const optionValueSettings = {
|
||||||
form: null,
|
form: null,
|
||||||
|
formDataDefaults: {},
|
||||||
getFormData: rule => ({}),
|
getFormData: rule => ({}),
|
||||||
assignRuleSettings: (rule, getter) => {},
|
assignRuleSettings: (rule, getter) => {},
|
||||||
validate: state => {}
|
validate: state => {}
|
||||||
|
@ -261,16 +338,24 @@ export function getRuleHelpers(t, fields) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, Object.keys(ruleHelpers.primitiveRuleTypes.text), stringValueSettings);
|
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, Object.keys(ruleHelpers.primitiveRuleTypes.website), stringValueSettings);
|
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, Object.keys(ruleHelpers.primitiveRuleTypes.number), numberValueSettings);
|
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.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'], birthdayRelativeValueSettings);
|
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
|
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'], dateRelativeValueSettings);
|
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, Object.keys(ruleHelpers.primitiveRuleTypes.option), optionValueSettings);
|
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], Object.keys(ruleHelpers.primitiveRuleTypes['dropdown-enum']), stringValueSettings);
|
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], Object.keys(ruleHelpers.primitiveRuleTypes['radio-enum']), stringValueSettings);
|
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: '',
|
||||||
|
direction: 'before'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ruleHelpers.getCompositeRuleTypeOptions = () => {
|
ruleHelpers.getCompositeRuleTypeOptions = () => {
|
||||||
|
@ -283,24 +368,22 @@ export function getRuleHelpers(t, fields) {
|
||||||
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||||
website: ['eq', 'like', 're'],
|
website: ['eq', 'like', 're'],
|
||||||
number: ['eq', 'lt', 'le', 'gt', 'ge'],
|
number: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||||
birthday: ['eq', 'lt', 'le', 'gt', 'ge', 'eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'],
|
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||||
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'],
|
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
|
||||||
option: ['isTrue', 'isFalse'],
|
option: ['isTrue', 'isFalse'],
|
||||||
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||||
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
|
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
|
||||||
};
|
};
|
||||||
|
|
||||||
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
|
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const predefColumns = [
|
const predefColumns = [
|
||||||
{
|
{
|
||||||
column: 'email',
|
column: 'email',
|
||||||
name: t('Email address'),
|
name: t('Email address'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
tag: 'EMAIL'
|
key: 'EMAIL'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
column: 'opt_in_country',
|
column: 'opt_in_country',
|
||||||
|
@ -329,7 +412,7 @@ export function getRuleHelpers(t, fields) {
|
||||||
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
|
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
|
||||||
];
|
];
|
||||||
|
|
||||||
ruleHelpers.fieldsByColumn = [];
|
ruleHelpers.fieldsByColumn = {};
|
||||||
for (const fld of ruleHelpers.fields) {
|
for (const fld of ruleHelpers.fields) {
|
||||||
ruleHelpers.fieldsByColumn[fld.column] = fld;
|
ruleHelpers.fieldsByColumn[fld.column] = fld;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Dropdown, Form,
|
Dropdown, Form,
|
||||||
withForm
|
withForm
|
||||||
} from '../../lib/form';
|
} from '../../lib/form';
|
||||||
import axios from '../../lib/axios';
|
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
@withForm
|
||||||
|
@ -24,62 +23,48 @@ export default class List extends Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const t = props.t;
|
const t = props.t;
|
||||||
this.state = {
|
this.state = {};
|
||||||
segmentOptions: [
|
|
||||||
{key: 'none', label: t('All subscriptions')}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.subscriptionStatusLabels = {
|
this.subscriptionStatusLabels = {
|
||||||
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
|
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
|
||||||
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
|
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
|
||||||
[SubscriptionStatus.BOUNCED]: t('Bounced'),
|
[SubscriptionStatus.BOUNCED]: t('Bounced'),
|
||||||
[SubscriptionStatus.COMPLAINED]: t('Complained'),
|
[SubscriptionStatus.COMPLAINED]: t('Complained'),
|
||||||
}
|
};
|
||||||
|
|
||||||
this.initForm({
|
this.initForm({
|
||||||
onChange: {
|
onChange: {
|
||||||
segment: ::this.onSegmentChange
|
segment: (newState, key, oldValue, value) => {
|
||||||
|
this.navigateTo(`/lists/${this.props.list.id}/subscriptions` + (value ? '/' + value : ''));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
list: PropTypes.object
|
list: PropTypes.object,
|
||||||
|
segments: PropTypes.array,
|
||||||
|
segmentId: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
onSegmentChange(state, attr, oldValue, newValue) {
|
updateSegmentSelection(props) {
|
||||||
// TODO
|
this.populateFormValues({
|
||||||
|
segment: props.segmentId || ''
|
||||||
this.subscriptionsTable.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
|
||||||
async loadSegmentOptions() {
|
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
const result = await axios.get(`/rest/segments/${this.props.list.id}`);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
segmentOptions: [
|
|
||||||
{key: 'none', label: t('All subscriptions')},
|
|
||||||
...result.data.map(x => ({ key: x.id.toString(), label: x.name})),
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.populateFormValues({
|
this.updateSegmentSelection(this.props);
|
||||||
segment: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadSegmentOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.updateSegmentSelection(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const list = this.props.list;
|
const list = this.props.list;
|
||||||
|
const segments = this.props.segments;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ data: 2, title: t('Email') },
|
{ data: 2, title: t('Email') },
|
||||||
|
@ -96,6 +81,17 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const segmentOptions = [
|
||||||
|
{key: '', label: t('All subscriptions')},
|
||||||
|
...segments.map(x => ({ key: x.id.toString(), label: x.name}))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
let dataUrl = '/rest/subscriptions-table/' + list.id;
|
||||||
|
if (this.props.segmentId) {
|
||||||
|
dataUrl += '/' + this.props.segmentId;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
@ -110,12 +106,12 @@ export default class List extends Component {
|
||||||
|
|
||||||
<div className="well well-sm">
|
<div className="well well-sm">
|
||||||
<Form format="inline" stateOwner={this}>
|
<Form format="inline" stateOwner={this}>
|
||||||
<Dropdown format="inline" className="input-sm" id="segment" label={t('Segment')} options={this.state.segmentOptions}/>
|
<Dropdown format="inline" className="input-sm" id="segment" label={t('Segment')} options={segmentOptions}/>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Table ref={node => this.subscriptionsTable = node} withHeader dataUrl={`/rest/subscriptions-table/${list.id}`} columns={columns} />
|
<Table ref={node => this.subscriptionsTable = node} withHeader dataUrl={dataUrl} columns={columns} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ const knex = require('knex')({
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/../setup/knex/migrations'
|
directory: __dirname + '/../setup/knex/migrations'
|
||||||
}
|
}
|
||||||
// , debug: true
|
, debug: true
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = knex;
|
module.exports = knex;
|
||||||
|
|
135
models/fields.js
135
models/fields.js
|
@ -113,10 +113,14 @@ async function getById(context, listId, id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listTx(tx, listId) {
|
||||||
|
return await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'key', 'column', 'order_list', 'order_subscribe', 'order_manage']).orderBy('id', 'asc');
|
||||||
|
}
|
||||||
|
|
||||||
async function list(context, listId) {
|
async function list(context, listId) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||||
return await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'key', 'column', 'order_list', 'order_subscribe', 'order_manage']).orderBy('id', 'asc');
|
return await listTx(tx, listId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,72 +129,76 @@ async function listByOrderListTx(tx, listId, extraColumns = []) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listDTAjax(context, listId, params) {
|
async function listDTAjax(context, listId, params) {
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await knex.transaction(async tx => {
|
||||||
context,
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||||
[{ entityTypeId: 'list', requiredOperations: ['manageFields'] }],
|
|
||||||
params,
|
|
||||||
builder => builder
|
|
||||||
.from('custom_fields')
|
|
||||||
.innerJoin('lists', 'custom_fields.list', 'lists.id')
|
|
||||||
|
|
||||||
// This self join is to provide 'option' fields a reference to their parent grouped field. If the field is not an option, it refers to itself
|
return await dtHelpers.ajaxListTx(
|
||||||
// All this is to show options always below their group parent
|
tx,
|
||||||
.innerJoin('custom_fields AS parent_fields', function() {
|
params,
|
||||||
this.on(function() {
|
builder => builder
|
||||||
this.on('custom_fields.type', '=', knex.raw('?', ['option']))
|
.from('custom_fields')
|
||||||
.on('custom_fields.group', '=', 'parent_fields.id');
|
|
||||||
}).orOn(function() {
|
// This self join is to provide 'option' fields a reference to their parent grouped field. If the field is not an option, it refers to itself
|
||||||
this.on('custom_fields.type', '<>', knex.raw('?', ['option']))
|
// All this is to show options always below their group parent
|
||||||
.on('custom_fields.id', '=', 'parent_fields.id');
|
.innerJoin('custom_fields AS parent_fields', function() {
|
||||||
});
|
this.on(function() {
|
||||||
})
|
this.on('custom_fields.type', '=', knex.raw('?', ['option']))
|
||||||
.where('custom_fields.list', listId),
|
.on('custom_fields.group', '=', 'parent_fields.id');
|
||||||
[ 'custom_fields.id', 'custom_fields.name', 'custom_fields.type', 'custom_fields.key', 'custom_fields.order_list' ],
|
}).orOn(function() {
|
||||||
{
|
this.on('custom_fields.type', '<>', knex.raw('?', ['option']))
|
||||||
orderByBuilder: (builder, orderColumn, orderDir) => {
|
.on('custom_fields.id', '=', 'parent_fields.id');
|
||||||
// We use here parent_fields to keep options always below their parent group
|
});
|
||||||
if (orderColumn === 'custom_fields.order_list') {
|
})
|
||||||
builder
|
.where('custom_fields.list', listId),
|
||||||
.orderBy(knex.raw('-parent_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc') // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
|
[ 'custom_fields.id', 'custom_fields.name', 'custom_fields.type', 'custom_fields.key', 'custom_fields.order_list' ],
|
||||||
.orderBy('parent_fields.name', orderDir)
|
{
|
||||||
.orderBy(knex.raw('custom_fields.type = "option"'), 'asc')
|
orderByBuilder: (builder, orderColumn, orderDir) => {
|
||||||
} else {
|
// We use here parent_fields to keep options always below their parent group
|
||||||
const parentColumn = orderColumn.replace(/^custom_fields/, 'parent_fields');
|
if (orderColumn === 'custom_fields.order_list') {
|
||||||
builder
|
builder
|
||||||
.orderBy(parentColumn, orderDir)
|
.orderBy(knex.raw('-parent_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc') // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
|
||||||
.orderBy('parent_fields.name', orderDir)
|
.orderBy('parent_fields.name', orderDir)
|
||||||
.orderBy(knex.raw('custom_fields.type = "option"'), 'asc');
|
.orderBy(knex.raw('custom_fields.type = "option"'), 'asc')
|
||||||
|
} else {
|
||||||
|
const parentColumn = orderColumn.replace(/^custom_fields/, 'parent_fields');
|
||||||
|
builder
|
||||||
|
.orderBy(parentColumn, orderDir)
|
||||||
|
.orderBy('parent_fields.name', orderDir)
|
||||||
|
.orderBy(knex.raw('custom_fields.type = "option"'), 'asc');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listGroupedDTAjax(context, listId, params) {
|
async function listGroupedDTAjax(context, listId, params) {
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await knex.transaction(async tx => {
|
||||||
context,
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||||
[{ entityTypeId: 'list', requiredOperations: ['manageFields'] }],
|
|
||||||
params,
|
return await dtHelpers.ajaxListTx(
|
||||||
builder => builder
|
tx,
|
||||||
.from('custom_fields')
|
params,
|
||||||
.innerJoin('lists', 'custom_fields.list', 'lists.id')
|
builder => builder
|
||||||
.where('custom_fields.list', listId)
|
.from('custom_fields')
|
||||||
.whereIn('custom_fields.type', groupedTypes),
|
.where('custom_fields.list', listId)
|
||||||
[ 'custom_fields.id', 'custom_fields.name', 'custom_fields.type', 'custom_fields.key', 'custom_fields.order_list' ],
|
.whereIn('custom_fields.type', groupedTypes),
|
||||||
{
|
['custom_fields.id', 'custom_fields.name', 'custom_fields.type', 'custom_fields.key', 'custom_fields.order_list'],
|
||||||
orderByBuilder: (builder, orderColumn, orderDir) => {
|
{
|
||||||
if (orderColumn === 'custom_fields.order_list') {
|
orderByBuilder: (builder, orderColumn, orderDir) => {
|
||||||
builder
|
if (orderColumn === 'custom_fields.order_list') {
|
||||||
.orderBy(knex.raw('-custom_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc') // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
|
builder
|
||||||
.orderBy('custom_fields.name', orderDir);
|
.orderBy(knex.raw('-custom_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc') // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
|
||||||
} else {
|
.orderBy('custom_fields.name', orderDir);
|
||||||
builder
|
} else {
|
||||||
.orderBy(orderColumn, orderDir)
|
builder
|
||||||
.orderBy('custom_fields.name', orderDir);
|
.orderBy(orderColumn, orderDir)
|
||||||
|
.orderBy('custom_fields.name', orderDir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function serverValidate(context, listId, data) {
|
async function serverValidate(context, listId, data) {
|
||||||
|
@ -374,7 +382,7 @@ async function removeTx(tx, context, listId, id) {
|
||||||
table.dropColumn(existing.column);
|
table.dropColumn(existing.column);
|
||||||
});
|
});
|
||||||
|
|
||||||
await segments.removeRulesByFieldIdTx(tx, context, listId, id);
|
await segments.removeRulesByColumnTx(tx, context, listId, existing.column);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,11 +399,12 @@ async function removeAllByListIdTx(tx, context, listId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is to handle circular dependency with segments.js
|
||||||
module.exports = {
|
Object.assign(module.exports, {
|
||||||
hash,
|
hash,
|
||||||
getById,
|
getById,
|
||||||
list,
|
list,
|
||||||
|
listTx,
|
||||||
listByOrderListTx,
|
listByOrderListTx,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
listGroupedDTAjax,
|
listGroupedDTAjax,
|
||||||
|
@ -404,4 +413,4 @@ module.exports = {
|
||||||
remove,
|
remove,
|
||||||
removeAllByListIdTx,
|
removeAllByListIdTx,
|
||||||
serverValidate
|
serverValidate
|
||||||
};
|
});
|
|
@ -6,16 +6,215 @@ const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
const { enforce, filterObject } = require('../lib/helpers');
|
const { enforce, filterObject } = require('../lib/helpers');
|
||||||
const hasher = require('node-object-hash')();
|
const hasher = require('node-object-hash')();
|
||||||
|
const moment = require('moment');
|
||||||
|
const fields = require('./fields');
|
||||||
|
|
||||||
|
const { parseDate, parseBirthday, DateFormat } = require('../shared/date');
|
||||||
|
|
||||||
const allowedKeys = new Set(['name', 'settings']);
|
const allowedKeys = new Set(['name', 'settings']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const predefColumns = [
|
||||||
|
{
|
||||||
|
column: 'email',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'opt_in_country',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'created',
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'latest_open',
|
||||||
|
type: 'date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: 'latest_click',
|
||||||
|
type: 'date'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const compositeRuleTypes = {
|
||||||
|
all: {
|
||||||
|
addQuery: (query, rules, addSubQuery) => {
|
||||||
|
for (const rule of rules) {
|
||||||
|
query.where(function() {
|
||||||
|
addSubQuery(this, rule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
some: {
|
||||||
|
addQuery: (query, rules, addSubQuery) => {
|
||||||
|
for (const rule of rules) {
|
||||||
|
query.orWhere(function() {
|
||||||
|
addSubQuery(this, rule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
none: {
|
||||||
|
addQuery: (query, rules, addSubQuery) => {
|
||||||
|
for (const rule of rules) {
|
||||||
|
query.whereNot(function() {
|
||||||
|
addSubQuery(this, rule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const primitiveRuleTypes = {
|
||||||
|
text: {},
|
||||||
|
website: {},
|
||||||
|
number: {},
|
||||||
|
date: {},
|
||||||
|
birthday: {},
|
||||||
|
option: {},
|
||||||
|
'dropdown-enum': {},
|
||||||
|
'radio-enum': {}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function stringValueSettings(sqlOperator, allowEmpty) {
|
||||||
|
return {
|
||||||
|
validate: rule => {
|
||||||
|
enforce(typeof rule.value === 'string', 'Invalid value type in rule');
|
||||||
|
enforce(allowEmpty || rule.value, 'Value in rule must not be empty');
|
||||||
|
},
|
||||||
|
addQuery: (query, rule) => query.where(rule.column, sqlOperator, rule.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValueSettings(sqlOperator) {
|
||||||
|
return {
|
||||||
|
validate: rule => {
|
||||||
|
enforce(typeof rule.value === 'number', 'Invalid value type in rule');
|
||||||
|
},
|
||||||
|
addQuery: (query, rule) => query.where(rule.column, sqlOperator, rule.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateValueSettings(thisDaySqlOperator, nextDaySqlOperator) {
|
||||||
|
return {
|
||||||
|
validate: rule => {
|
||||||
|
const date = moment.utc(rule.value);
|
||||||
|
enforce(date.isValid(), 'Invalid date value');
|
||||||
|
},
|
||||||
|
addQuery: (query, rule) => {
|
||||||
|
const thisDay = moment.utc(rule.value).startOf('day');
|
||||||
|
const nextDay = moment(thisDay).add(1, 'days');
|
||||||
|
|
||||||
|
if (thisDaySqlOperator) {
|
||||||
|
query.where(rule.column, thisDaySqlOperator, thisDay.toDate())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextDaySqlOperator) {
|
||||||
|
query.where(rule.column, nextDaySqlOperator, nextDay.toDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateRelativeValueSettings(todaySqlOperator, tomorrowSqlOperator) {
|
||||||
|
return {
|
||||||
|
validate: rule => {
|
||||||
|
enforce(typeof rule.value === 'number', 'Invalid value type in rule');
|
||||||
|
},
|
||||||
|
addQuery: (query, rule) => {
|
||||||
|
const todayWithOffset = moment.utc().startOf('day').add(rule.value, 'days');
|
||||||
|
const tomorrowWithOffset = moment(todayWithOffset).add(1, 'days');
|
||||||
|
|
||||||
|
if (todaySqlOperator) {
|
||||||
|
query.where(rule.column, todaySqlOperator, todayWithOffset.toDate())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tomorrowSqlOperator) {
|
||||||
|
query.where(rule.column, tomorrowSqlOperator, tomorrowWithOffset.toDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionValueSettings(value) {
|
||||||
|
return {
|
||||||
|
validate: rule => {},
|
||||||
|
addQuery: (query, rule) => query.where(rule.column, value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
primitiveRuleTypes.text.eq = stringValueSettings('=', true);
|
||||||
|
primitiveRuleTypes.text.like = stringValueSettings('LIKE', true);
|
||||||
|
primitiveRuleTypes.text.re = stringValueSettings('REGEXP', true);
|
||||||
|
primitiveRuleTypes.text.lt = stringValueSettings('<', false);
|
||||||
|
primitiveRuleTypes.text.le = stringValueSettings('<=', false);
|
||||||
|
primitiveRuleTypes.text.gt = stringValueSettings('>', false);
|
||||||
|
primitiveRuleTypes.text.ge = stringValueSettings('>=', false);
|
||||||
|
|
||||||
|
primitiveRuleTypes.website.eq = stringValueSettings('=', true);
|
||||||
|
primitiveRuleTypes.website.like = stringValueSettings('LIKE', true);
|
||||||
|
primitiveRuleTypes.website.re = stringValueSettings('REGEXP', true);
|
||||||
|
|
||||||
|
primitiveRuleTypes.number.eq = numberValueSettings('=');
|
||||||
|
primitiveRuleTypes.number.lt = numberValueSettings('<');
|
||||||
|
primitiveRuleTypes.number.le = numberValueSettings('<=');
|
||||||
|
primitiveRuleTypes.number.gt = numberValueSettings('>');
|
||||||
|
primitiveRuleTypes.number.ge = numberValueSettings('>=');
|
||||||
|
|
||||||
|
primitiveRuleTypes.date.eq = dateValueSettings('>=', '<');
|
||||||
|
primitiveRuleTypes.date.lt = dateValueSettings('<', null);
|
||||||
|
primitiveRuleTypes.date.le = dateValueSettings(null, '<');
|
||||||
|
primitiveRuleTypes.date.gt = dateValueSettings(null, '>=');
|
||||||
|
primitiveRuleTypes.date.ge = dateValueSettings('>=', null);
|
||||||
|
|
||||||
|
primitiveRuleTypes.date.eqTodayPlusDays = dateRelativeValueSettings('>=', '<');
|
||||||
|
primitiveRuleTypes.date.ltTodayPlusDays = dateRelativeValueSettings('<', null);
|
||||||
|
primitiveRuleTypes.date.leTodayPlusDays = dateRelativeValueSettings(null, '<');
|
||||||
|
primitiveRuleTypes.date.gtTodayPlusDays = dateRelativeValueSettings(null, '>=');
|
||||||
|
primitiveRuleTypes.date.geTodayPlusDays = dateRelativeValueSettings('>=', null);
|
||||||
|
|
||||||
|
primitiveRuleTypes.birthday.eq = dateValueSettings('>=', '<');
|
||||||
|
primitiveRuleTypes.birthday.lt = dateValueSettings('<', null);
|
||||||
|
primitiveRuleTypes.birthday.le = dateValueSettings(null, '<');
|
||||||
|
primitiveRuleTypes.birthday.gt = dateValueSettings(null, '>=');
|
||||||
|
primitiveRuleTypes.birthday.ge = dateValueSettings('>=', null);
|
||||||
|
|
||||||
|
primitiveRuleTypes.option.isTrue = optionValueSettings(true);
|
||||||
|
primitiveRuleTypes.option.isFalse = optionValueSettings(false);
|
||||||
|
|
||||||
|
primitiveRuleTypes['dropdown-enum'].eq = stringValueSettings('=', true);
|
||||||
|
primitiveRuleTypes['dropdown-enum'].like = stringValueSettings('LIKE', true);
|
||||||
|
primitiveRuleTypes['dropdown-enum'].re = stringValueSettings('REGEXP', true);
|
||||||
|
primitiveRuleTypes['dropdown-enum'].lt = stringValueSettings('<', false);
|
||||||
|
primitiveRuleTypes['dropdown-enum'].le = stringValueSettings('<=', false);
|
||||||
|
primitiveRuleTypes['dropdown-enum'].gt = stringValueSettings('>', false);
|
||||||
|
primitiveRuleTypes['dropdown-enum'].ge = stringValueSettings('>=', false);
|
||||||
|
|
||||||
|
primitiveRuleTypes['radio-enum'].eq = stringValueSettings('=', true);
|
||||||
|
primitiveRuleTypes['radio-enum'].like = stringValueSettings('LIKE', true);
|
||||||
|
primitiveRuleTypes['radio-enum'].re = stringValueSettings('REGEXP', true);
|
||||||
|
primitiveRuleTypes['radio-enum'].lt = stringValueSettings('<', false);
|
||||||
|
primitiveRuleTypes['radio-enum'].le = stringValueSettings('<=', false);
|
||||||
|
primitiveRuleTypes['radio-enum'].gt = stringValueSettings('>', false);
|
||||||
|
primitiveRuleTypes['radio-enum'].ge = stringValueSettings('>=', false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function hash(entity) {
|
function hash(entity) {
|
||||||
return hasher.hash(filterObject(entity, allowedKeys));
|
return hasher.hash(filterObject(entity, allowedKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listDTAjax(context, listId, params) {
|
async function listDTAjax(context, listId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
|
||||||
|
|
||||||
return await dtHelpers.ajaxListTx(
|
return await dtHelpers.ajaxListTx(
|
||||||
tx,
|
tx,
|
||||||
|
@ -30,7 +229,7 @@ async function listDTAjax(context, listId, params) {
|
||||||
|
|
||||||
async function list(context, listId) {
|
async function list(context, listId) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
|
||||||
|
|
||||||
return await tx('segments').select(['id', 'name']).where('list', listId).orderBy('name', 'asc');
|
return await tx('segments').select(['id', 'name']).where('list', listId).orderBy('name', 'asc');
|
||||||
});
|
});
|
||||||
|
@ -38,18 +237,53 @@ async function list(context, listId) {
|
||||||
|
|
||||||
async function getById(context, listId, id) {
|
async function getById(context, listId, id) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
|
||||||
const entity = await tx('segments').where({id, list: listId}).first();
|
const entity = await tx('segments').where({id, list: listId}).first();
|
||||||
entity.settings = JSON.parse(entity.settings);
|
entity.settings = JSON.parse(entity.settings);
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _validateAndPreprocess(tx, listId, entity, isCreate) {
|
||||||
|
enforce(entity.name, 'Name must be present');
|
||||||
|
enforce(entity.settings, 'Settings must be present');
|
||||||
|
enforce(entity.settings.rootRule, 'Root rule must be present in setting');
|
||||||
|
enforce(entity.settings.rootRule.type in compositeRuleTypes, 'Root rule must be composite');
|
||||||
|
|
||||||
|
|
||||||
|
const flds = await fields.listTx(tx, listId);
|
||||||
|
const allowedFlds = [
|
||||||
|
...predefColumns,
|
||||||
|
...flds.filter(fld => fld.type in primitiveRuleTypes)
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldsByColumn = {};
|
||||||
|
for (const fld of allowedFlds) {
|
||||||
|
fieldsByColumn[fld.column] = fld;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRule(rule) {
|
||||||
|
if (rule.type in compositeRuleTypes) {
|
||||||
|
for (const childRule of rule.rules) {
|
||||||
|
validateRule(childRule);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const colType = fieldsByColumn[rule.column].type;
|
||||||
|
primitiveRuleTypes[colType][rule.type].validate(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
validateRule(entity.settings.rootRule);
|
||||||
|
|
||||||
|
entity.settings = JSON.stringify(entity.settings);
|
||||||
|
}
|
||||||
|
|
||||||
async function create(context, listId, entity) {
|
async function create(context, listId, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
||||||
|
|
||||||
entity.settings = JSON.stringify(entity.settings);
|
await _validateAndPreprocess(tx, listId, entity, true);
|
||||||
|
|
||||||
const filteredEntity = filterObject(entity, allowedKeys);
|
const filteredEntity = filterObject(entity, allowedKeys);
|
||||||
filteredEntity.list = listId;
|
filteredEntity.list = listId;
|
||||||
|
@ -77,9 +311,9 @@ async function updateWithConsistencyCheck(context, listId, entity) {
|
||||||
throw new interoperableErrors.ChangedError();
|
throw new interoperableErrors.ChangedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.settings = JSON.stringify(entity.settings);
|
await _validateAndPreprocess(tx, listId, entity, false);
|
||||||
|
|
||||||
await tx('segments').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
await tx('segments').where({list: listId, id: entity.id}).update(filterObject(entity, allowedKeys));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +322,7 @@ async function removeTx(tx, context, listId, id) {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
||||||
|
|
||||||
// The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
|
// The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
|
||||||
await tx('segments').where({list: listId, id: id}).del();
|
await tx('segments').where({list: listId, id}).del();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(context, listId, id) {
|
async function remove(context, listId, id) {
|
||||||
|
@ -100,15 +334,70 @@ async function remove(context, listId, id) {
|
||||||
async function removeAllByListIdTx(tx, context, listId) {
|
async function removeAllByListIdTx(tx, context, listId) {
|
||||||
const entities = await tx('segments').where('list', listId).select(['id']);
|
const entities = await tx('segments').where('list', listId).select(['id']);
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
await removeTx(tx, context, entity.id);
|
await removeTx(tx, context, listId, entity.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRulesByFieldIdTx(tx, context, listId, fieldId) {
|
async function removeRulesByColumnTx(tx, context, listId, column) {
|
||||||
// FIXME
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
||||||
|
|
||||||
|
function pruneChildRules(rule) {
|
||||||
|
if (rule.type in compositeRuleTypes) {
|
||||||
|
|
||||||
|
const newRules = [];
|
||||||
|
|
||||||
|
for (const childRule of rule.rules) {
|
||||||
|
if (childRule.column !== column) {
|
||||||
|
pruneChildRules(childRule);
|
||||||
|
newRules.push(childRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.rules = newRules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = await tx('segments').where({list: listId});
|
||||||
|
for (const entity of entities) {
|
||||||
|
const settings = JSON.parse(entity.settings);
|
||||||
|
|
||||||
|
pruneChildRules(settings.rootRule);
|
||||||
|
|
||||||
|
await tx('segments').where({list: listId, id: entity.id}).update('settings', JSON.stringify(settings));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
async function getQueryGeneratorTx(tx, listId, id) {
|
||||||
|
const flds = await fields.listTx(tx, listId);
|
||||||
|
const allowedFlds = [
|
||||||
|
...predefColumns,
|
||||||
|
...flds.filter(fld => fld.type in primitiveRuleTypes)
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldsByColumn = {};
|
||||||
|
for (const fld of allowedFlds) {
|
||||||
|
fieldsByColumn[fld.column] = fld;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = await tx('segments').where({id, list: listId}).first();
|
||||||
|
const settings = JSON.parse(entity.settings);
|
||||||
|
|
||||||
|
function processRule(query, rule) {
|
||||||
|
if (rule.type in compositeRuleTypes) {
|
||||||
|
compositeRuleTypes[rule.type].addQuery(query, rule.rules, (subQuery, childRule) => {
|
||||||
|
processRule(subQuery, childRule);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const colType = fieldsByColumn[rule.column].type;
|
||||||
|
primitiveRuleTypes[colType][rule.type].addQuery(query, rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query => processRule(query, settings.rootRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is to handle circular dependency with fields.js
|
||||||
|
Object.assign(module.exports, {
|
||||||
hash,
|
hash,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
list,
|
list,
|
||||||
|
@ -117,5 +406,6 @@ module.exports = {
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
remove,
|
remove,
|
||||||
removeAllByListIdTx,
|
removeAllByListIdTx,
|
||||||
removeRulesByFieldIdTx
|
removeRulesByColumnTx,
|
||||||
};
|
getQueryGeneratorTx
|
||||||
|
});
|
|
@ -6,6 +6,8 @@ const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
const fields = require('./fields');
|
const fields = require('./fields');
|
||||||
const { SubscriptionStatus } = require('../shared/lists');
|
const { SubscriptionStatus } = require('../shared/lists');
|
||||||
|
const segments = require('./segments');
|
||||||
|
|
||||||
|
|
||||||
const allowedKeysBase = new Set(['cid', 'email']);
|
const allowedKeysBase = new Set(['cid', 'email']);
|
||||||
|
|
||||||
|
@ -18,16 +20,23 @@ function hash(entity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function listDTAjax(context, listId, params) {
|
async function listDTAjax(context, listId, segmentId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||||
|
|
||||||
const flds = await fields.listByOrderListTx(tx, listId, ['column']);
|
const flds = await fields.listByOrderListTx(tx, listId, ['column']);
|
||||||
|
const addSegmentQuery = segmentId ? await segments.getQueryGeneratorTx(tx, listId, segmentId) : () => {};
|
||||||
|
|
||||||
return await dtHelpers.ajaxListTx(
|
return await dtHelpers.ajaxListTx(
|
||||||
tx,
|
tx,
|
||||||
params,
|
params,
|
||||||
builder => builder.from(`subscription__${listId}`),
|
builder => {
|
||||||
|
const query = builder.from(`subscription__${listId}`);
|
||||||
|
query.where(function() {
|
||||||
|
addSegmentQuery(this);
|
||||||
|
});
|
||||||
|
return query;
|
||||||
|
},
|
||||||
['id', 'cid', 'email', 'status', 'created', ...flds.map(fld => fld.column)]
|
['id', 'cid', 'email', 'status', 'created', ...flds.map(fld => fld.column)]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,8 +6,8 @@ const subscriptions = require('../../models/subscriptions');
|
||||||
const router = require('../../lib/router-async').create();
|
const router = require('../../lib/router-async').create();
|
||||||
|
|
||||||
|
|
||||||
router.postAsync('/subscriptions-table/:listId', passport.loggedIn, async (req, res) => {
|
router.postAsync('/subscriptions-table/:listId/:segmentId?', passport.loggedIn, async (req, res) => {
|
||||||
return res.json(await subscriptions.listDTAjax(req.context, req.params.listId, req.body));
|
return res.json(await subscriptions.listDTAjax(req.context, req.params.listId, req.params.segmentId, req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -77,26 +77,50 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'birthday':
|
case 'birthday':
|
||||||
case 'date':
|
if (oldSettings.range) {
|
||||||
if (oldSettings.relativeRange) {
|
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'geNowPlusDays', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start});
|
||||||
rules.push({ type: 'leNowPlusDays', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end});
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
rules: [
|
rules: [
|
||||||
{ type: 'geNowPlusDays', column: oldRule.column, value: oldSettings.start},
|
{ type: 'ge', column: oldRule.column, value: oldSettings.start},
|
||||||
{ type: 'leNowPlusDays', column: oldRule.column, value: oldSettings.end}
|
{ type: 'le', column: oldRule.column, value: oldSettings.end}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (oldSettings.start) {
|
} else if (oldSettings.start) {
|
||||||
rules.push({ type: 'geNowPlusDays', column: oldRule.column, value: oldSettings.startDirection ? oldSettings.start : -oldSettings.start });
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start });
|
||||||
}
|
}
|
||||||
if (oldSettings.end) {
|
if (oldSettings.end) {
|
||||||
rules.push({ type: 'leNowPlusDays', column: oldRule.column, value: oldSettings.endDirection ? oldSettings.end : -oldSettings.end });
|
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
if (oldSettings.relativeRange) {
|
||||||
|
if (oldSettings.start && oldSettings.end) {
|
||||||
|
if (type === 'all') {
|
||||||
|
rules.push({ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.start});
|
||||||
|
rules.push({ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.end});
|
||||||
|
} else {
|
||||||
|
rules.push({
|
||||||
|
type: 'all',
|
||||||
|
rules: [
|
||||||
|
{ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.start},
|
||||||
|
{ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.end}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (oldSettings.start) {
|
||||||
|
rules.push({ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.startDirection ? oldSettings.start : -oldSettings.start });
|
||||||
|
}
|
||||||
|
if (oldSettings.end) {
|
||||||
|
rules.push({ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.endDirection ? oldSettings.end : -oldSettings.end });
|
||||||
}
|
}
|
||||||
} else if (oldSettings.range) {
|
} else if (oldSettings.range) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
|
|
67
shared/date.js
Normal file
67
shared/date.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
const birthdayYear = 2000;
|
||||||
|
|
||||||
|
const DateFormat = {
|
||||||
|
US: 'us',
|
||||||
|
EU: 'eur',
|
||||||
|
INTL: 'intl'
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateFormatStrings = {
|
||||||
|
'us': 'MM/DD/YYYY',
|
||||||
|
'eur': 'DD/MM/YYYY',
|
||||||
|
'intl': 'YYYY-MM-DD'
|
||||||
|
};
|
||||||
|
|
||||||
|
const birthdayFormatStrings = {
|
||||||
|
'us': 'MM/DD',
|
||||||
|
'eur': 'DD/MM',
|
||||||
|
'intl': 'MM/DD'
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseDate(format, text) {
|
||||||
|
const date = moment.utc(text, dateFormatStrings[format]);
|
||||||
|
|
||||||
|
if (date.isValid()) {
|
||||||
|
return date.toDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBirthday(format, text) {
|
||||||
|
const fullDateStr = format === DateFormat.INTL ? birthdayYear + '-' + text : text + '-' + birthdayYear;
|
||||||
|
const date = moment.utc(fullDateStr, dateFormatStrings[format]);
|
||||||
|
|
||||||
|
if (date.isValid()) {
|
||||||
|
return date.toDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(format, date) {
|
||||||
|
return moment.utc(date).format(dateFormatStrings[format]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBirthday(format, date) {
|
||||||
|
return moment.utc(date).format(birthdayFormatStrings[format]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateFormatString(format) {
|
||||||
|
return dateFormatStrings[format];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBirthdayFormatString(format) {
|
||||||
|
return birthdayFormatStrings[format];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DateFormat,
|
||||||
|
birthdayYear,
|
||||||
|
parseDate,
|
||||||
|
parseBirthday,
|
||||||
|
formatDate,
|
||||||
|
formatBirthday,
|
||||||
|
getDateFormatString,
|
||||||
|
getBirthdayFormatString
|
||||||
|
};
|
|
@ -1,65 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
function parseDate(type, text) {
|
|
||||||
const isUs = type === 'us';
|
|
||||||
const trimmedText = text.trim();
|
|
||||||
|
|
||||||
// try international format first YYYY-MM-DD
|
|
||||||
const parts = trimmedText.match(/^(\d{4})\D+(\d{2})(?:\D+(\d{2})\b)$/);
|
|
||||||
let day, month, year;
|
|
||||||
let value;
|
|
||||||
|
|
||||||
if (parts) {
|
|
||||||
year = Number(parts[1]) || 2000;
|
|
||||||
month = Number(parts[2]) || 0;
|
|
||||||
day = Number(parts[3]) || 0;
|
|
||||||
value = new Date(Date.UTC(year, month - 1, day));
|
|
||||||
} else {
|
|
||||||
const parts = trimmedText.match(/^(\d+)\D+(\d+)(?:\D+(\d+)\b)$/);
|
|
||||||
if (!parts) {
|
|
||||||
value = null;
|
|
||||||
} else {
|
|
||||||
day = Number(parts[isUs ? 2 : 1]);
|
|
||||||
month = Number(parts[isUs ? 1 : 2]);
|
|
||||||
year = Number(parts[3]);
|
|
||||||
|
|
||||||
if (!day || !month) {
|
|
||||||
value = null;
|
|
||||||
} else {
|
|
||||||
value = new Date(Date.UTC(year, month - 1, day));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseBirthday(type, text) {
|
|
||||||
const isUs = type === 'us';
|
|
||||||
const trimmedText = text.trim();
|
|
||||||
|
|
||||||
let day, month, year;
|
|
||||||
let value;
|
|
||||||
|
|
||||||
const parts = trimmedText.match(/^(\d+)\D+(\d+)$/);
|
|
||||||
if (!parts) {
|
|
||||||
value = null;
|
|
||||||
} else {
|
|
||||||
day = Number(parts[isUs ? 2 : 1]);
|
|
||||||
month = Number(parts[isUs ? 1 : 2]);
|
|
||||||
|
|
||||||
if (!day || !month) {
|
|
||||||
value = null;
|
|
||||||
} else {
|
|
||||||
value = new Date(Date.UTC(2000, month - 1, day));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(value);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
parseDate,
|
|
||||||
parseBirthday
|
|
||||||
};
|
|
Loading…
Reference in a new issue