WiP on segments

This commit is contained in:
Tomas Bures 2017-08-19 15:12:22 +02:00
parent 6cc34136f5
commit f3ff89c536
21 changed files with 945 additions and 352 deletions

View file

@ -27,6 +27,7 @@
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-ace": "^5.1.0",
"react-day-picker": "^6.1.0",
"react-dom": "^15.6.1",
"react-i18next": "^4.6.1",
"react-router-dom": "^4.1.1",

View file

@ -19,6 +19,14 @@ import 'brace/mode/json';
import 'brace/mode/handlebars';
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 = {
Loading: 0,
LoadingWithNotice: 1,
@ -79,7 +87,7 @@ class Form extends Component {
if (!owner.isFormReady()) {
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 {
return <div></div>;
}
@ -91,7 +99,7 @@ class Form extends Component {
</fieldset>
{statusMessageText &&
<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>
}
</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 {
static propTypes = {
id: PropTypes.string.isRequired,
@ -374,7 +487,7 @@ class ButtonRow extends Component {
}
render() {
let className = 'mt-button-row';
let className = styles.buttonRow;
if (this.props.className) {
className += ' ' + this.props.className;
}
@ -554,13 +667,13 @@ class TableSelect extends Component {
if (props.dropdown) {
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<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}/>
<span className="input-group-btn">
<ActionButton label={t('Select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
</span>
</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}/>
</div>
</div>
@ -834,13 +947,29 @@ function withForm(target) {
this.setState(previousState => {
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 = {
formState: previousState.formState.withMutations(mutState => {
mutState.setIn(['data', key, 'value'], value);
validateFormState(this, mutState);
})
formState
};
const onChangeCallback = this.state.formSettings.onChange || {};
if (typeof onChangeCallback === 'object') {
@ -1000,6 +1129,7 @@ export {
InputField,
CheckBox,
TextArea,
DatePicker,
Dropdown,
AlignedRow,
ButtonRow,

View file

@ -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;
}

View file

@ -5,12 +5,12 @@ import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom'
import './page.css';
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import interoperableErrors from '../../../shared/interoperable-errors';
import { DismissibleAlert, Button } from './bootstrap-components';
import mailtrainConfig from 'mailtrainConfig';
import axios from '../lib/axios';
import styles from "./styles.scss";
class Breadcrumb extends Component {
@ -133,7 +133,7 @@ class SecondaryNavBar extends Component {
}
if (renderedElems.length > 1) {
let className = 'mt-secondary-nav nav nav-pills';
let className = styles.secondaryNav + ' nav nav-pills';
if (this.props.className) {
className += ' ' + this.props.className;
}
@ -482,7 +482,7 @@ class Toolbar extends Component {
};
render() {
let className = 'pull-right mt-button-row';
let className = 'pull-right ' + styles.buttonRow;
if (this.props.className) {
className += ' ' + this.props.className;
}

View 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;
}

View file

@ -15,6 +15,7 @@ import axios from './axios';
import { withPageHelpers } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import styles from "./styles.scss";
//dtFactory();
//dtSelectFactory();
@ -169,7 +170,7 @@ class Table extends Component {
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() {
@ -179,7 +180,7 @@ class Table extends Component {
for (const column of columns) {
if (column.actions) {
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 options = {};
@ -322,19 +323,20 @@ class Table extends Component {
if (this.props.data) {
this.table.clear();
this.table.rows.add(this.props.data);
} else {
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.refresh();
}
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.fetchAndNotifySelectionData();
}

View file

@ -14,6 +14,7 @@ import axios from './axios';
import { withPageHelpers } from '../lib/page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import styles from "./styles.scss";
const TreeSelectMode = {
NONE: 0,
@ -122,7 +123,7 @@ class TreeTable extends Component {
}
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);
for (const {label, link} of actions) {

View file

@ -14,7 +14,8 @@ import { getFieldTypes } from './field-types';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
import { parseDate, parseBirthday } from '../../../../shared/fields';
import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date';
import styles from "../../lib/styles.scss";
@translate()
@withForm
@ -72,7 +73,7 @@ export default class CUD extends Component {
}
data.enumOptions = '';
data.dateFormat = 'eur';
data.dateFormat = DateFormat.EUR;
data.renderTemplate = '';
switch (data.type) {
@ -374,8 +375,8 @@ export default class CUD extends Component {
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD/YYYY')},
{key: 'eur', label: t('DD/MM/YYYY')}
{key: DateFormat.US, label: t('MM/DD/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>}/>
@ -387,8 +388,8 @@ export default class CUD extends Component {
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD')},
{key: 'eur', label: t('DD/MM')}
{key: DateFormat.US, label: t('MM/DD')},
{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>}/>
@ -445,7 +446,7 @@ export default class CUD extends Component {
<InputField id="name" label={t('Name')}/>
{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}/>
}

View file

@ -40,9 +40,13 @@ const getStructure = t => {
navs: {
subscriptions: {
title: t('Subscribers'),
resolve: {
segments: params => `/rest/segments/${params.listId}`
},
extraParams: [':segmentId?'],
link: params => `/lists/${params.listId}/subscriptions`,
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)': {
title: t('Edit'),
@ -163,7 +167,7 @@ const getStructure = t => {
export default function() {
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')
);
};

View file

@ -93,6 +93,10 @@ export default class CUD extends Component {
await this.getFormValuesFromURL(`/rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
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
this.setState({
rulesTree: this.getTreeFromRules(data.settings.rootRule.rules)
});
});
}

View file

@ -27,6 +27,7 @@ export default class CUD extends Component {
this.state = {};
this.initForm({
onChangeBeforeValidation: ::this.populateRuleDefaults,
onChange: ::this.onFormChange
});
}
@ -45,12 +46,12 @@ export default class CUD extends Component {
const ruleHelpers = this.ruleHelpers;
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);
if (settings) {
data = settings.getFormData(rule);
} else {
data = {}; // This handles the case of a new rule, which does not have a type and column yet
Object.assign(data, settings.getFormData(rule));
}
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']);
if (!ruleHelpers.isCompositeRuleType(ruleType)) {
const columnType = state.getIn(['column', 'value']);
const column = state.getIn(['column', 'value']);
if (columnType) {
const colType = ruleHelpers.getColumnType(columnType);
if (column) {
const colType = ruleHelpers.getColumnType(column);
if (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) {
const noErrors = !newState.formState.get('data').find(attr => attr.get('error'));
@ -136,7 +158,6 @@ export default class CUD extends Component {
}
}
render() {
const t = this.props.t;
const rule = this.props.rule;
@ -153,7 +174,7 @@ export default class CUD extends Component {
{ 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} />;
let ruleTypeSelect = null;
@ -186,6 +207,7 @@ export default class CUD extends Component {
}
return (
<div className={styles.ruleOptions}>
<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')}/>
</ButtonRow>
</Form>
</div>
);
}

View file

@ -1,14 +1,12 @@
'use strict';
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) {
function formatDate(date) {
return date; // FIXME
}
const ruleHelpers = {};
ruleHelpers.compositeRuleTypes = {
@ -108,49 +106,72 @@ export function getRuleHelpers(t, fields) {
}
}
ruleHelpers.primitiveRuleTypes.date = ruleHelpers.primitiveRuleTypes.birthday = {
ruleHelpers.primitiveRuleTypes.date = {
eq: {
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: {
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: {
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: {
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: {
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: {
dropdownLabel: t('On x-th day before/after now'),
eqTodayPlusDays: {
dropdownLabel: t('On x-th day before/after current date'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is'),
},
ltNowPlusDays: {
dropdownLabel: t('Before x-th day before/after now'),
ltTodayPlusDays: {
dropdownLabel: t('Before x-th day before/after current date'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before'),
},
leNowPlusDays: {
dropdownLabel: t('Before or on x-th day before/after now'),
leTodayPlusDays: {
dropdownLabel: t('Before or on x-th day before/after current date'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before or on'),
},
gtNowPlusDays: {
dropdownLabel: t('After x-th day before/after now'),
gtTodayPlusDays: {
dropdownLabel: t('After x-th day before/after current date'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after'),
},
geNowPlusDays: {
dropdownLabel: t('After or on x-th day before/after now'),
geTodayPlusDays: {
dropdownLabel: t('After or on x-th day before/after current date'),
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 = {
isTrue: {
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')} />,
getFormData: rule => ({
value: rule.value
@ -203,13 +224,13 @@ export function getRuleHelpers(t, fields) {
rule.value = getter('value');
},
validate: state => {
if (!state.getIn(['value', 'value'])) {
if (!allowEmpty && !state.getIn(['value', 'value'])) {
state.setIn(['value', 'error'], t('Value must not be empty'));
} else {
state.setIn(['value', 'error'], null);
}
}
};
});
const numberValueSettings = {
form: <InputField id="value" label={t('Value')} />,
@ -232,23 +253,79 @@ export function getRuleHelpers(t, fields) {
};
const birthdayValueSettings = {
form: <InputField id="value" label={t('Value')} /> // FIXME
};
const birthdayRelativeValueSettings = {
form: <InputField id="value" label={t('Value')} /> // FIXME
form: <DatePicker id="value" label={t('Date')} birthday />,
getFormData: rule => ({
value: formatBirthday(DateFormat.INTL, rule.value)
}),
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 = {
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 = {
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 = {
form: null,
formDataDefaults: {},
getFormData: rule => ({}),
assignRuleSettings: (rule, getter) => {},
validate: state => {}
@ -261,16 +338,24 @@ export function getRuleHelpers(t, fields) {
}
}
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, Object.keys(ruleHelpers.primitiveRuleTypes.text), stringValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, Object.keys(ruleHelpers.primitiveRuleTypes.website), stringValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, Object.keys(ruleHelpers.primitiveRuleTypes.number), numberValueSettings);
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.birthday, ['eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'], birthdayRelativeValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'], dateRelativeValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, Object.keys(ruleHelpers.primitiveRuleTypes.option), optionValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], Object.keys(ruleHelpers.primitiveRuleTypes['dropdown-enum']), stringValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], Object.keys(ruleHelpers.primitiveRuleTypes['radio-enum']), stringValueSettings);
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: '',
direction: 'before'
};
ruleHelpers.getCompositeRuleTypeOptions = () => {
@ -283,24 +368,22 @@ export function getRuleHelpers(t, fields) {
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
website: ['eq', 'like', 're'],
number: ['eq', 'lt', 'le', 'gt', 'ge'],
birthday: ['eq', 'lt', 'le', 'gt', 'ge', 'eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'],
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'],
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
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 }));
};
const predefColumns = [
{
column: 'email',
name: t('Email address'),
type: 'text',
tag: 'EMAIL'
key: 'EMAIL'
},
{
column: 'opt_in_country',
@ -329,7 +412,7 @@ export function getRuleHelpers(t, fields) {
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
];
ruleHelpers.fieldsByColumn = [];
ruleHelpers.fieldsByColumn = {};
for (const fld of ruleHelpers.fields) {
ruleHelpers.fieldsByColumn[fld.column] = fld;
}

View file

@ -12,7 +12,6 @@ import {
Dropdown, Form,
withForm
} from '../../lib/form';
import axios from '../../lib/axios';
@translate()
@withForm
@ -24,62 +23,48 @@ export default class List extends Component {
super(props);
const t = props.t;
this.state = {
segmentOptions: [
{key: 'none', label: t('All subscriptions')}
]
};
this.state = {};
this.subscriptionStatusLabels = {
[SubscriptionStatus.SUBSCRIBED]: t('Subscribed'),
[SubscriptionStatus.UNSUBSCRIBED]: t('Unubscribed'),
[SubscriptionStatus.BOUNCED]: t('Bounced'),
[SubscriptionStatus.COMPLAINED]: t('Complained'),
}
};
this.initForm({
onChange: {
segment: ::this.onSegmentChange
segment: (newState, key, oldValue, value) => {
this.navigateTo(`/lists/${this.props.list.id}/subscriptions` + (value ? '/' + value : ''));
}
}
});
}
static propTypes = {
list: PropTypes.object
list: PropTypes.object,
segments: PropTypes.array,
segmentId: PropTypes.string
}
onSegmentChange(state, attr, oldValue, newValue) {
// TODO
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})),
]
updateSegmentSelection(props) {
this.populateFormValues({
segment: props.segmentId || ''
});
}
componentDidMount() {
this.populateFormValues({
segment: 'none'
});
this.loadSegmentOptions();
this.updateSegmentSelection(this.props);
}
componentWillReceiveProps(nextProps) {
this.updateSegmentSelection(nextProps);
}
render() {
const t = this.props.t;
const list = this.props.list;
const segments = this.props.segments;
const columns = [
{ 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 (
<div>
<Toolbar>
@ -110,12 +106,12 @@ export default class List extends Component {
<div className="well well-sm">
<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>
</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>
);
}