work in progress on segments

some cleanup of models - handling dependencies in delete
This commit is contained in:
Tomas Bures 2017-08-14 22:53:29 +02:00
parent b23529a75b
commit 0bfb30817b
29 changed files with 553 additions and 990 deletions

View file

@ -91,9 +91,11 @@ class TreeTable extends Component {
const data = [];
for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry);
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>)
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>)
entry.children = this.sanitizeTreeData(entry.children);
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>);
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>);
if (entry.children) {
entry.children = this.sanitizeTreeData(entry.children);
}
data.push(entry);
}
return data;

View file

@ -81,7 +81,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving list ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.form === 'default') {

View file

@ -11,7 +11,6 @@ import {
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types';
import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
@ -45,6 +44,7 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
fields: PropTypes.array,
entity: PropTypes.object
}
@ -58,27 +58,6 @@ export default class CUD extends Component {
}
}
@withAsyncErrorHandler
async loadOrderOptions() {
const t = this.props.t;
const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
this.setState({
orderListOptions: getOrderOptions('order_list'),
orderSubscribeOptions: getOrderOptions('order_subscribe'),
orderManageOptions: getOrderOptions('order_manage')
});
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
@ -139,8 +118,6 @@ export default class CUD extends Component {
orderManageOptions: []
});
}
this.loadOrderOptions();
}
localValidateFormValues(state) {
@ -250,7 +227,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving field ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') {
@ -320,6 +297,16 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
...this.props.fields.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
const type = this.getFormValue('type');
@ -469,9 +456,9 @@ export default class CUD extends Component {
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={getOrderOptions('order_list')} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={getOrderOptions('order_subscribe')} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={getOrderOptions('order_manage')} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}

View file

@ -323,7 +323,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving forms ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.selectedTemplate;

View file

@ -14,6 +14,7 @@ import FieldsList from './fields/List';
import FieldsCUD from './fields/CUD';
import SubscriptionsList from './subscriptions/List';
import SegmentsList from './segments/List';
import SegmentsCUD from './segments/CUD';
import Share from '../shares/Share';
@ -58,20 +59,24 @@ const getStructure = t => {
':fieldId([0-9]+)': {
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
resolve: {
field: params => `/rest/fields/${params.listId}/${params.fieldId}`
field: params => `/rest/fields/${params.listId}/${params.fieldId}`,
fields: params => `/rest/fields/${params.listId}`
},
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
render: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} />
render: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
create: {
title: t('Create Field'),
render: props => <FieldsCUD action="create" list={props.resolved.list} />
title: t('Create'),
resolve: {
fields: params => `/rest/fields/${params.listId}`
},
render: props => <FieldsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
@ -79,7 +84,31 @@ const getStructure = t => {
title: t('Segments'),
link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('manageSegments'),
render: props => <SegmentsList list={props.resolved.list} />
render: props => <SegmentsList list={props.resolved.list} />,
children: {
':segmentId([0-9]+)': {
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
resolve: {
segment: params => `/rest/segments/${params.listId}/${params.segmentId}`,
fields: params => `/rest/fields/${params.listId}`
},
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
render: props => <SegmentsCUD action={props.match.params.action} entity={props.resolved.segment} list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
create: {
title: t('Create'),
resolve: {
fields: params => `/rest/fields/${params.listId}`
},
render: props => <SegmentsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
share: {
title: t('Share'),

View file

@ -5,17 +5,12 @@ import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
withForm, Form, FormSendMethod, InputField, ButtonRow, Button
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types';
import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
import { parseDate, parseBirthday } from '../../../../shared/fields';
import {TreeTable} from "../../lib/tree";
@translate()
@withForm
@ -28,119 +23,28 @@ export default class CUD extends Component {
this.state = {};
this.fieldTypes = getFieldTypes(props.t);
this.initForm({
serverValidation: {
url: `/rest/fields-validate/${this.props.list.id}`,
changed: ['key'],
extra: ['id']
},
onChange: {
name: ::this.onChangeName
}
});
this.initForm();
}
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
fields: PropTypes.array,
entity: PropTypes.object
}
onChangeName(state, attr, oldValue, newValue) {
const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
const oldKey = state.formState.getIn(['data', 'key', 'value']);
if (oldKey === '' || oldKey === oldComputedKey) {
const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
state.formState = state.formState.setIn(['data', 'key', 'value'], newKey);
}
}
@withAsyncErrorHandler
async loadOrderOptions() {
const t = this.props.t;
const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
this.setState({
orderListOptions: getOrderOptions('order_list'),
orderSubscribeOptions: getOrderOptions('order_subscribe'),
orderManageOptions: getOrderOptions('order_manage')
});
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.settings = data.settings || {};
if (data.default_value === null) {
data.default_value = '';
}
if (data.type !== 'option') {
data.group = null;
}
data.enumOptions = '';
data.dateFormat = 'eur';
data.renderTemplate = '';
switch (data.type) {
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
case 'json':
data.renderTemplate = data.settings.renderTemplate;
break;
case 'radio-enum':
case 'dropdown-enum':
data.enumOptions = this.renderEnumOptions(data.settings.enumOptions);
data.renderTemplate = data.settings.renderTemplate;
break;
case 'date':
case 'birthday':
data.dateFormat = data.settings.dateFormat;
break;
}
data.orderListBefore = data.orderListBefore.toString();
data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
data.orderManageBefore = data.orderManageBefore.toString();
// FIXME populate all others from settings
});
} else {
this.populateFormValues({
name: '',
type: 'text',
key: '',
default_value: '',
group: null,
renderTemplate: '',
enumOptions: '',
dateFormat: 'eur',
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
orderSubscribeBefore: 'end',
orderManageBefore: 'end',
orderListOptions: [],
orderSubscribeOptions: [],
orderManageOptions: []
settingsJSON: ''
});
}
this.loadOrderOptions();
}
localValidateFormValues(state) {
@ -152,151 +56,31 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null);
}
const keyServerValidation = state.getIn(['key', 'serverValidation']);
if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
} else if (!keyServerValidation) {
state.setIn(['key', 'error'], t('Validation is in progress...'));
} else if (keyServerValidation.exists) {
state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
} else {
state.setIn(['key', 'error'], null);
}
const type = state.getIn(['type', 'value']);
const group = state.getIn(['group', 'value']);
if (type === 'option' && !group) {
state.setIn(['group', 'error'], t('Group has to be selected'));
} else {
state.setIn(['group', 'error'], null);
}
const defaultValue = state.getIn(['default_value', 'value']);
if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
state.setIn(['default_value', 'error'], t('Default value is not integer number'));
} else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date'));
} else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date'));
} else {
state.setIn(['default_value', 'error'], null);
}
if (type === 'radio-enum' || type === 'dropdown-enum') {
const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
if (enumOptions.errors) {
state.setIn(['enumOptions', 'error'], <div>{enumOptions.errors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
} else {
state.setIn(['enumOptions', 'error'], null);
if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
}
}
} else {
state.setIn(['enumOptions', 'error'], null);
}
}
parseEnumOptions(text) {
const t = this.props.t;
const errors = [];
const options = {};
const lines = text.split('\n');
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx].trim();
if (line != '') {
const matches = line.match(/^([^|]*)[|](.*)$/);
if (matches) {
const key = matches[1].trim();
const label = matches[2].trim();
options[key] = label;
} else {
errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
}
}
}
if (errors.length) {
return {
errors
};
} else {
return {
options
};
}
}
renderEnumOptions(options) {
return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n');
}
async submitHandler() {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
url = `/rest/segments/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `/rest/fields/${this.props.list.id}`
url = `/rest/segments/${this.props.list.id}`
}
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving field ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') {
data.default_value = null;
}
if (data.type !== 'option') {
data.group = null;
}
data.settings = {};
switch (data.type) {
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
case 'json':
data.settings.renderTemplate = data.renderTemplate;
break;
case 'radio-enum':
case 'dropdown-enum':
data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options;
data.settings.renderTemplate = data.renderTemplate;
break;
case 'date':
case 'birthday':
data.settings.dateFormat = data.dateFormat;
break;
}
delete data.renderTemplate;
delete data.enumOptions;
delete data.dateFormat;
if (data.type === 'option') {
data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
} else {
data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
}
// FIXME - make sure settings is correct and delete all others
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved'));
this.enableForm();
this.setFormStatusMessage('success', t('Segment saved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
@ -320,123 +104,50 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
const sampleTreeData = [
{
key: 'a',
title: 'A',
expanded: true,
children: [
{
key: 'aa',
title: 'AA',
expanded: true,
children: [
{
key: 'aaa',
title: 'AAA',
expanded: true
},
{
key: 'aab',
title: 'AAB',
expanded: true
}
]
},
{
key: 'ab',
title: 'AB',
expanded: true,
children: [
{
key: 'aba',
title: 'ABA',
expanded: true
},
{
key: 'abb',
title: 'ABB',
expanded: true
}
]
},
]
}
];
const type = this.getFormValue('type');
let fieldSettings = null;
switch (type) {
case 'text':
case 'website':
case 'longtext':
case 'gpg':
case 'number':
fieldSettings =
<Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array, for
example <code>{'{{#each values}} {{this}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'radio-enum':
case 'dropdown-enum':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="enumOptions"
label={t('Options')}
height="250px"
mode="text"
help={<Trans><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
<div><code>au|Australia</code></div><div><code>at|Austria</code></div></Trans>}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array.
Each entry in the array is an object with attributes <code>key</code> and <code>label</code>.
For example <code>{'{{#each values}} {{this.value}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'date':
fieldSettings =
<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')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'birthday':
fieldSettings =
<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')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'json':
fieldSettings = <Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="json"
help={<Trans>You can use this template to render JSON values (if the JSON is an array then the array is
exposed as <code>values</code>, otherwise you can access the JSON keys directly).</Trans>}
/>
</Fieldset>;
break;
case 'option':
const fieldsGroupedColumns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }
];
fieldSettings =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
}
return (
@ -445,40 +156,26 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/fields/${this.props.list.id}`}
deletingMsg={t('Deleting field ...')}
deletedMsg={t('Field deleted')}/>
deleteUrl={`/rest/segments/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/segments/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/segments/${this.props.list.id}`}
deletingMsg={t('Deleting segment ...')}
deletedMsg={t('Segment deleted')}/>
}
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
{isEdit ?
<StaticField id="type" className="mt-form-disabled" label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
<InputField id="key" label={t('Merge tag')}/>
{fieldSettings}
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
</ButtonRow>
<hr />
<TreeTable data={sampleTreeData} />
</Form>
</div>
);

View file

@ -30,7 +30,6 @@ export default class List extends Component {
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Match') },
{
actions: data => [{
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,

View file

@ -1,486 +0,0 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types';
import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
import { parseDate, parseBirthday } from '../../../../shared/fields';
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
constructor(props) {
super(props);
this.state = {};
this.fieldTypes = getFieldTypes(props.t);
this.initForm({
serverValidation: {
url: `/rest/fields-validate/${this.props.list.id}`,
changed: ['key'],
extra: ['id']
},
onChange: {
name: ::this.onChangeName
}
});
}
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
entity: PropTypes.object
}
onChangeName(state, attr, oldValue, newValue) {
const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
const oldKey = state.formState.getIn(['data', 'key', 'value']);
if (oldKey === '' || oldKey === oldComputedKey) {
const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
state.formState = state.formState.setIn(['data', 'key', 'value'], newKey);
}
}
@withAsyncErrorHandler
async loadOrderOptions() {
const t = this.props.t;
const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
this.setState({
orderListOptions: getOrderOptions('order_list'),
orderSubscribeOptions: getOrderOptions('order_subscribe'),
orderManageOptions: getOrderOptions('order_manage')
});
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.settings = data.settings || {};
if (data.default_value === null) {
data.default_value = '';
}
if (data.type !== 'option') {
data.group = null;
}
data.enumOptions = '';
data.dateFormat = 'eur';
data.renderTemplate = '';
switch (data.type) {
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
case 'json':
data.renderTemplate = data.settings.renderTemplate;
break;
case 'radio-enum':
case 'dropdown-enum':
data.enumOptions = this.renderEnumOptions(data.settings.enumOptions);
data.renderTemplate = data.settings.renderTemplate;
break;
case 'date':
case 'birthday':
data.dateFormat = data.settings.dateFormat;
break;
}
data.orderListBefore = data.orderListBefore.toString();
data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
data.orderManageBefore = data.orderManageBefore.toString();
});
} else {
this.populateFormValues({
name: '',
type: 'text',
key: '',
default_value: '',
group: null,
renderTemplate: '',
enumOptions: '',
dateFormat: 'eur',
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
orderSubscribeBefore: 'end',
orderManageBefore: 'end',
orderListOptions: [],
orderSubscribeOptions: [],
orderManageOptions: []
});
}
this.loadOrderOptions();
}
localValidateFormValues(state) {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
const keyServerValidation = state.getIn(['key', 'serverValidation']);
if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
} else if (!keyServerValidation) {
state.setIn(['key', 'error'], t('Validation is in progress...'));
} else if (keyServerValidation.exists) {
state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
} else {
state.setIn(['key', 'error'], null);
}
const type = state.getIn(['type', 'value']);
const group = state.getIn(['group', 'value']);
if (type === 'option' && !group) {
state.setIn(['group', 'error'], t('Group has to be selected'));
} else {
state.setIn(['group', 'error'], null);
}
const defaultValue = state.getIn(['default_value', 'value']);
if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
state.setIn(['default_value', 'error'], t('Default value is not integer number'));
} else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date'));
} else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date'));
} else {
state.setIn(['default_value', 'error'], null);
}
if (type === 'radio-enum' || type === 'dropdown-enum') {
const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
if (enumOptions.errors) {
state.setIn(['enumOptions', 'error'], <div>{enumOptions.errors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
} else {
state.setIn(['enumOptions', 'error'], null);
if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
}
}
} else {
state.setIn(['enumOptions', 'error'], null);
}
}
parseEnumOptions(text) {
const t = this.props.t;
const errors = [];
const options = {};
const lines = text.split('\n');
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx].trim();
if (line != '') {
const matches = line.match(/^([^|]*)[|](.*)$/);
if (matches) {
const key = matches[1].trim();
const label = matches[2].trim();
options[key] = label;
} else {
errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
}
}
}
if (errors.length) {
return {
errors
};
} else {
return {
options
};
}
}
renderEnumOptions(options) {
return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n');
}
async submitHandler() {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `/rest/fields/${this.props.list.id}`
}
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving field ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') {
data.default_value = null;
}
if (data.type !== 'option') {
data.group = null;
}
data.settings = {};
switch (data.type) {
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
case 'json':
data.settings.renderTemplate = data.renderTemplate;
break;
case 'radio-enum':
case 'dropdown-enum':
data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options;
data.settings.renderTemplate = data.renderTemplate;
break;
case 'date':
case 'birthday':
data.settings.dateFormat = data.dateFormat;
break;
}
delete data.renderTemplate;
delete data.enumOptions;
delete data.dateFormat;
if (data.type === 'option') {
data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
} else {
data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
}
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
}
} catch (error) {
if (error instanceof interoperableErrors.DependencyNotFoundError) {
this.setFormStatusMessage('danger',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{t('It seems that another field upon which sort field order was established has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
</span>
);
return;
}
throw error;
}
}
render() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
const type = this.getFormValue('type');
let fieldSettings = null;
switch (type) {
case 'text':
case 'website':
case 'longtext':
case 'gpg':
case 'number':
fieldSettings =
<Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array, for
example <code>{'{{#each values}} {{this}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'radio-enum':
case 'dropdown-enum':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="enumOptions"
label={t('Options')}
height="250px"
mode="text"
help={<Trans><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
<div><code>au|Australia</code></div><div><code>at|Austria</code></div></Trans>}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array.
Each entry in the array is an object with attributes <code>key</code> and <code>label</code>.
For example <code>{'{{#each values}} {{this.value}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'date':
fieldSettings =
<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')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'birthday':
fieldSettings =
<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')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'json':
fieldSettings = <Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="json"
help={<Trans>You can use this template to render JSON values (if the JSON is an array then the array is
exposed as <code>values</code>, otherwise you can access the JSON keys directly).</Trans>}
/>
</Fieldset>;
break;
case 'option':
const fieldsGroupedColumns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }
];
fieldSettings =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
}
return (
<div>
{isEdit &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/fields/${this.props.list.id}`}
deletingMsg={t('Deleting field ...')}
deletedMsg={t('Field deleted')}/>
}
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
{isEdit ?
<StaticField id="type" className="mt-form-disabled" label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
<InputField id="key" label={t('Merge tag')}/>
{fieldSettings}
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -125,7 +125,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving namespace ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);

View file

@ -134,7 +134,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving report ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const params = {};

View file

@ -244,7 +244,7 @@ export default class CUD extends Component {
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving report template ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);

View file

@ -141,7 +141,7 @@ export default class CUD extends Component {
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving user ...'));
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.password2;