work in progress on segments
some cleanup of models - handling dependencies in delete
This commit is contained in:
parent
b23529a75b
commit
0bfb30817b
29 changed files with 553 additions and 990 deletions
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue