'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 interoperableErrors from '../../../../shared/interoperable-errors'; import validators from '../../../../shared/validators'; import slugify from 'slugify'; import { parseDate, parseBirthday, DateFormat } from '../../../../shared/date'; import styles from "../../lib/styles.scss"; @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, 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); } } 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 = DateFormat.EUR; data.renderTemplate = ''; switch (data.type) { case 'checkbox-grouped': 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 / 'end' / 'none' orderSubscribeBefore: 'end', orderManageBefore: 'end', orderListOptions: [], orderSubscribeOptions: [], orderManageOptions: [] }); } } 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'],
{enumOptions.errors.map((err, idx) =>
{err}
)}
); } 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 ...')); 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-grouped': 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', {t('Your updates cannot be saved.')}{' '} {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.')} ); return; } throw error; } } render() { 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'); let fieldSettings = null; switch (type) { case 'text': case 'website': case 'longtext': case 'gpg': case 'number': fieldSettings =
; break; case 'checkbox-grouped': case 'radio-grouped': case 'dropdown-grouped': fieldSettings =
You can control the appearance of the merge tag with this template. The template uses handlebars syntax and you can find all values from {'{{values}}'} array, for example {'{{#each values}} {{this}} {{/each}}'}. If template is not defined then multiple values are joined with commas.} />
; break; case 'radio-enum': case 'dropdown-enum': fieldSettings =
Specify the options to select from in the following format:key|label. For example:
au|Australia
at|Austria
} /> Default key (e.g. au used when the field is empty.')}/> You can control the appearance of the merge tag with this template. The template uses handlebars syntax and you can find all values from {'{{values}}'} array. Each entry in the array is an object with attributes key and label. For example {'{{#each values}} {{this.value}} {{/each}}'}. If template is not defined then multiple values are joined with commas.} />
; break; case 'date': fieldSettings =
Default value used when the field is empty.}/>
; break; case 'birthday': fieldSettings =
Default value used when the field is empty.}/>
; break; case 'json': fieldSettings =
Default key (e.g. au used when the field is empty.')}/> You can use this template to render JSON values (if the JSON is an array then the array is exposed as values, otherwise you can access the JSON keys directly).} />
; 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 =
; break; } return (
{isEdit && } {isEdit ? t('Edit Field') : t('Create Field')}
{isEdit ? {(this.fieldTypes[this.getFormValue('type')] || {}).label} : } {fieldSettings} {type !== 'option' &&
}
); } }