diff --git a/client/package.json b/client/package.json index 1c050ad3..515670ff 100644 --- a/client/package.json +++ b/client/package.json @@ -30,6 +30,7 @@ "react-dom": "^15.6.1", "react-i18next": "^4.6.1", "react-router-dom": "^4.1.1", + "slugify": "^1.1.0", "url-parse": "^1.1.9" }, "devDependencies": { diff --git a/client/src/lib/form.js b/client/src/lib/form.js index e2494c9f..b39ac952 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -143,7 +143,8 @@ class StaticField extends Component { static propTypes = { id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, - help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) + help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + className: PropTypes.string } render() { @@ -152,8 +153,13 @@ class StaticField extends Component { const id = this.props.id; const htmlId = 'form_' + id; + let className = 'form-control'; + if (props.className) { + className += ' ' + props.className; + } + return wrapInput(null, htmlId, owner, props.label, props.help, -
{props.children}
+
{props.children}
); } } diff --git a/client/src/lib/page.css b/client/src/lib/page.css index 28bfa709..f67709fb 100644 --- a/client/src/lib/page.css +++ b/client/src/lib/page.css @@ -23,6 +23,11 @@ display: block; } +.mt-form-disabled { + background-color: #eeeeee; + opacity: 1; +} + .ace_editor { border: 1px solid #ccc; } diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js index b47afd90..b3289d24 100644 --- a/client/src/lists/fields/CUD.js +++ b/client/src/lists/fields/CUD.js @@ -6,7 +6,7 @@ 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 + Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField } from '../../lib/form'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import {DeleteModalDialog} from "../../lib/delete"; @@ -14,6 +14,8 @@ 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 @@ -33,6 +35,9 @@ export default class CUD extends Component { url: `/rest/fields-validate/${this.props.list.id}`, changed: ['key'], extra: ['id'] + }, + onChange: { + name: ::this.onChangeName } }); } @@ -43,6 +48,16 @@ export default class CUD extends Component { 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; @@ -70,7 +85,34 @@ export default class CUD extends Component { if (data.default_value === null) { data.default_value = ''; } - // TODO: Construct form fields from settings + + 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; + } }); } else { @@ -81,6 +123,8 @@ export default class CUD extends Component { default_value: '', group: null, renderTemplate: '', + enumOptions: '', + dateFormat: 'eur', orderListBefore: 'end', // possible values are / 'end' / 'none' orderSubscribeBefore: 'end', orderManageBefore: 'end', @@ -113,13 +157,71 @@ export default class CUD extends Component { state.setIn(['key', 'error'], null); } - // TODO: Validate field settings: - // TODO: parse and check options for enums - // TODO: make sure group is selected for option - // TODO: check default date/birthday is in the right format - // TODO: check number is a number + 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); + } + + let enumOptionErrors; + if ((type === 'radio-enum' || type === 'dropdown-enum') && (enumOptionErrors = this.parseEnumOptions(state.getIn(['enumOptions', 'value'])).errors)) { + state.setIn(['enumOptions', 'error'],
{enumOptionErrors.map((err, idx) =>
{err}
)}
); + } 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; @@ -141,7 +243,34 @@ export default class CUD extends Component { data.default_value = null; } - // TODO: Construct settings field + 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 (submitSuccessful) { @@ -169,7 +298,7 @@ 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 typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label})); const type = this.getFormValue('type'); @@ -240,7 +369,7 @@ export default class CUD extends Component { {key: 'eur', label: t('DD/MM/YYYY')} ]} /> - Default value used when the field is empty.')}/> + Default value used when the field is empty.}/> ; break; @@ -253,7 +382,7 @@ export default class CUD extends Component { {key: 'eur', label: t('DD/MM')} ]} /> - Default value used when the field is empty.')}/> + Default value used when the field is empty.}/> ; break; @@ -306,7 +435,11 @@ export default class CUD extends Component {
- + {isEdit ? + {(this.fieldTypes[this.getFormValue('type')] || {}).label} + : + + } diff --git a/shared/fields.js b/shared/fields.js new file mode 100644 index 00000000..d283a339 --- /dev/null +++ b/shared/fields.js @@ -0,0 +1,65 @@ +'use strict'; + +function parseDate(type, text) { + const isUs = type === 'us'; + const trimmedText = text.trim(); + + // try international format first YYYY-MM-DD + const parts = trimmedText.match(/^(\d{4})\D+(\d{2})(?:\D+(\d{2})\b)$/); + let day, month, year; + let value; + + if (parts) { + year = Number(parts[1]) || 2000; + month = Number(parts[2]) || 0; + day = Number(parts[3]) || 0; + value = new Date(Date.UTC(year, month - 1, day)); + } else { + const parts = trimmedText.match(/^(\d+)\D+(\d+)(?:\D+(\d+)\b)$/); + if (!parts) { + value = null; + } else { + day = Number(parts[isUs ? 2 : 1]); + month = Number(parts[isUs ? 1 : 2]); + year = Number(parts[3]); + + if (!day || !month) { + value = null; + } else { + value = new Date(Date.UTC(year, month - 1, day)); + } + } + } + + return value; +} + +function parseBirthday(type, text) { + const isUs = type === 'us'; + const trimmedText = text.trim(); + + let day, month, year; + let value; + + const parts = trimmedText.match(/^(\d+)\D+(\d+)$/); + if (!parts) { + value = null; + } else { + day = Number(parts[isUs ? 2 : 1]); + month = Number(parts[isUs ? 1 : 2]); + + if (!day || !month) { + value = null; + } else { + value = new Date(Date.UTC(2000, month - 1, day)); + } + } + console.log(value); + + return value; +} + +module.exports = { + parseDate, + parseBirthday +}; \ No newline at end of file