From eb2287f6e95b1a887471a9f4878a2c7a43bac372 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Thu, 29 Jun 2017 23:22:33 +0200 Subject: [PATCH] Release candidate of basic user management - currently only CRUD on users, no permission assignment. --- client/src/lib/form.js | 1131 +++++++++-------- client/src/lib/page.css | 44 +- client/src/namespaces/CUD.js | 2 +- client/src/users/CUD.js | 34 +- lib/tools-async.js | 16 +- models/users.js | 104 +- routes/users.js | 33 +- setup/knex/migrations/20170506102634_base.js | 148 +-- .../20170617123450_create_user_name.js | 5 +- shared/validators.js | 9 + 10 files changed, 776 insertions(+), 750 deletions(-) create mode 100644 shared/validators.js diff --git a/client/src/lib/form.js b/client/src/lib/form.js index a411081d..916e2adf 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -1,557 +1,574 @@ -'use strict'; - -import React, { Component } from 'react'; -import axios from './axios'; -import Immutable from 'immutable'; -import { translate } from 'react-i18next'; -import PropTypes from 'prop-types'; -import interoperableErrors from '../../../shared/interoperable-errors'; -import { withPageHelpers } from './page' -import { withErrorHandling, withAsyncErrorHandler } from './error-handling'; -import { TreeTable, TreeSelectMode } from './tree'; - -const FormState = { - Loading: 0, - LoadingWithNotice: 1, - Ready: 2 -}; - -const FormSendMethod = { - PUT: 0, - POST: 1 -}; - -@translate() -@withPageHelpers -@withErrorHandling -class Form extends Component { - static propTypes = { - stateOwner: PropTypes.object.isRequired, - onSubmitAsync: PropTypes.func - } - - static childContextTypes = { - formStateOwner: PropTypes.object - } - - getChildContext() { - return { - formStateOwner: this.props.stateOwner - }; - } - - @withAsyncErrorHandler - async onSubmit(evt) { - const t = this.props.t; - - const owner = this.props.stateOwner; - - try { - evt.preventDefault(); - - if (this.props.onSubmitAsync) { - await this.props.onSubmitAsync(evt); - } - } catch (error) { - if (error instanceof interoperableErrors.ChangedError) { - owner.disableForm(); - owner.setFormStatusMessage('danger', - - {t('Your updates cannot be saved.')}{' '} - {t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')} - - ); - return; - } - - throw error; - } - } - - render() { - const t = this.props.t; - const owner = this.props.stateOwner; - const props = this.props; - const statusMessageText = owner.getFormStatusMessageText(); - const statusMessageSeverity = owner.getFormStatusMessageSeverity(); - - if (!owner.isFormReady()) { - if (owner.isFormWithLoadingNotice()) { - return

{t('Loading ...')}

- } else { - return
; - } - } else { - return ( -
-
- {props.children} -
- {statusMessageText &&

{statusMessageText}

} -
- ); - } - } -} - -function wrapInput(id, htmlId, owner, label, input) { - return ( -
-
- -
-
- {input} -
-
{owner.getFormValidationMessage(id)}
-
- ); -} - -class InputField extends Component { - static propTypes = { - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - placeholder: PropTypes.string - } - - static contextTypes = { - formStateOwner: PropTypes.object.isRequired - } - - render() { - const props = this.props; - const owner = this.context.formStateOwner; - const id = this.props.id; - const htmlId = 'form_' + id; - - return wrapInput(id, htmlId, owner, props.label, - owner.updateFormValue(id, evt.target.value)}/> - ); - } -} - -class TextArea extends Component { - static propTypes = { - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - placeholder: PropTypes.string - } - - static contextTypes = { - formStateOwner: PropTypes.object.isRequired - } - - render() { - const props = this.props; - const owner = this.context.formStateOwner; - const id = this.props.id; - const htmlId = 'form_' + id; - - return wrapInput(id, htmlId, owner, props.label, - - ); - } -} - -class ButtonRow extends Component { - render() { - return ( -
-
- {this.props.children} -
-
- ); - } -} - -@withErrorHandling -class Button extends Component { - static propTypes = { - onClickAsync: PropTypes.func, - onClick: PropTypes.func, - label: PropTypes.string, - icon: PropTypes.string, - className: PropTypes.string, - type: PropTypes.string - } - - static contextTypes = { - formStateOwner: PropTypes.object.isRequired - } - - @withAsyncErrorHandler - async onClick(evt) { - if (this.props.onClick) { - evt.preventDefault(); - - onClick(evt); - - } else if (this.props.onClickAsync) { - evt.preventDefault(); - - this.context.formStateOwner.disableForm(); - await this.props.onClickAsync(evt); - this.context.formStateOwner.enableForm(); - } - } - - render() { - const props = this.props; - - let className = 'btn'; - if (props.className) { - className = className + ' ' + props.className; - } - - let type = props.type || 'button'; - - let icon; - if (props.icon) { - icon = - } - - let iconSpacer; - if (props.icon && props.label) { - iconSpacer = ' '; - } - - return ( - - ); - } -} - -class TreeTableSelect extends Component { - static propTypes = { - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - dataUrl: PropTypes.string, - data: PropTypes.array - } - - static contextTypes = { - formStateOwner: PropTypes.object.isRequired - } - - async onSelectionChangedAsync(sel) { - const owner = this.context.formStateOwner; - owner.updateFormValue(this.props.id, sel); - } - - render() { - const props = this.props; - const owner = this.context.formStateOwner; - const id = this.props.id; - const htmlId = 'form_' + id; - - return ( -
-
- -
-
- -
-
{owner.getFormValidationMessage(id)}
-
- ); - } -} - -function withForm(target) { - const inst = target.prototype; - - const cleanFormState = Immutable.Map({ - state: FormState.Loading, - isValidationShown: false, - isDisabled: false, - statusMessageText: '', - data: Immutable.Map(), - isServerValidationRunning: false - }); - - inst.initFormState = function(serverValidationUrl, serverValidationAttrs) { - const state = this.state || {}; - state.formState = cleanFormState; - if (serverValidationUrl) { - state.formStateServerValidation = { url: serverValidationUrl, attrs: serverValidationAttrs }; - } - this.state = state; - }; - - inst.resetFormState = function() { - this.setState({ - formState: cleanFormState - }); - }; - - inst.getFormValuesFromURL = async function(url, mutator) { - setTimeout(() => { - this.setState(previousState => { - if (previousState.formState.get('state') === FormState.Loading) { - return { - formState: previousState.formState.set('state', FormState.LoadingWithNotice) - }; - } - }); - }, 500); - - const response = await axios.get(url); - - const data = response.data; - - data.originalHash = data.hash; - delete data.hash; - - if (mutator) { - mutator(data); - } - - this.populateFormValues(data); - }; - - inst.validateAndSendFormValuesToURL = async function(method, url, mutator) { - await this.waitForFormServerValidated(); - - if (this.isFormWithoutErrors()) { - const data = this.getFormValues(); - - if (mutator) { - mutator(data); - } - - if (method === FormSendMethod.PUT) { - await axios.put(url, data); - } else if (method === FormSendMethod.POST) { - await axios.post(url, data); - } - return true; - } else { - this.showFormValidation(); - return false; - } - }; - - - inst.populateFormValues = function(data) { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - mutState.set('state', FormState.Ready); - - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - for (const key in data) { - mutStateData.set(key, Immutable.Map({ - value: data[key] - })); - } - })); - - this.validateForm(mutState); - }) - })); - }; - - - // formValidateResolve is called by "validateForm" once client receives validation response from server that does not - // trigger another server validation - let formValidateResolve = null; - - const scheduleValidateForm = (self) => { - setTimeout(() => { - self.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - self.validateForm(mutState); - }) - })); - }, 0); - }; - - inst.waitForFormServerValidated = async function() { - if (!this.isFormServerValidated()) { - await new Promise(resolve => { formValidateResolve = resolve; }); - } - }; - - inst.validateForm = function(mutState) { - const serverValidation = this.state.formStateServerValidation; - - if (!mutState.get('isServerValidationRunning') && serverValidation) { - const payload = {}; - let payloadNotEmpty = false; - - for (const attr of serverValidation.attrs) { - const currValue = mutState.getIn(['data', attr, 'value']); - const serverValue = mutState.getIn(['data', attr, 'serverValue']); - - // This really assumes that all form values are preinitialized (i.e. not undef) - if (currValue !== serverValue) { - mutState.setIn(['data', attr, 'serverValidated'], false); - payload[attr] = currValue; - payloadNotEmpty = true; - } - } - - if (payloadNotEmpty) { - mutState.set('isServerValidationRunning', true); - - axios.post(serverValidation.url, payload) - .then(response => { - - this.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - mutState.set('isServerValidationRunning', false); - - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - for (const attr in payload) { - mutStateData.setIn([attr, 'serverValue'], payload[attr]); - - if (payload[attr] === mutState.getIn(['data', attr, 'value'])) { - mutStateData.setIn([attr, 'serverValidated'], true); - mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true); - } - } - })); - }) - })); - - scheduleValidateForm(this); - }) - .catch(error => { - console.log('Ignoring unhandled error in "validateForm": ' + error); - scheduleValidateForm(this); - }); - } else { - if (formValidateResolve) { - const resolve = formValidateResolve; - formValidateResolve = null; - resolve(); - } - } - } - - if (this.localValidateFormValues) { - mutState.update('data', stateData => stateData.withMutations(mutStateData => { - this.localValidateFormValues(mutStateData); - })); - } - }; - - inst.updateFormValue = function(key, value) { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(mutState => { - mutState.setIn(['data', key, 'value'], value); - this.validateForm(mutState); - }) - })); - }; - - inst.getFormValue = function(name) { - return this.state.formState.getIn(['data', name, 'value']); - }; - - inst.getFormValues = function(name) { - return this.state.formState.get('data').map(attr => attr.get('value')).toJS(); - }; - - inst.getFormError = function(name) { - return this.state.formState.getIn(['data', name, 'error']); - }; - - inst.isFormWithLoadingNotice = function() { - return this.state.formState.get('state') === FormState.LoadingWithNotice; - }; - - inst.isFormLoading = function() { - return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice; - }; - - inst.isFormReady = function() { - return this.state.formState.get('state') === FormState.Ready; - }; - - inst.isFormValidationShown = function() { - return this.state.formState.get('isValidationShown'); - }; - - inst.addFormValidationClass = function(className, name) { - if (this.isFormValidationShown()) { - const error = this.getFormError(name); - if (error) { - return className + ' has-error'; - } else { - return className + ' has-success'; - } - } else { - return className; - } - }; - - inst.getFormValidationMessage = function(name) { - if (this.isFormValidationShown()) { - return this.getFormError(name); - } else { - return ''; - } - }; - - inst.showFormValidation = function() { - this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)})); - }; - - inst.hideFormValidation = function() { - this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)})); - }; - - inst.isFormWithoutErrors = function() { - return !this.state.formState.get('data').find(attr => attr.get('error')); - }; - - inst.isFormServerValidated = function() { - return this.state.formStateServerValidation.attrs.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated'])); - }; - - inst.getFormStatusMessageText = function() { - return this.state.formState.get('statusMessageText'); - }; - - inst.getFormStatusMessageSeverity = function() { - return this.state.formState.get('statusMessageSeverity'); - }; - - inst.setFormStatusMessage = function(severity, text) { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(map => { - map.set('statusMessageText', text); - map.set('statusMessageSeverity', severity); - }) - })); - }; - - inst.enableForm = function() { - this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)})); - }; - - inst.disableForm = function() { - this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)})); - }; - - inst.isFormDisabled = function() { - return this.state.formState.get('isDisabled'); - }; - - return target; -} - - -export { - withForm, - Form, - InputField, - TextArea, - ButtonRow, - Button, - TreeTableSelect, - FormSendMethod -} +'use strict'; + +import React, { Component } from 'react'; +import axios from './axios'; +import Immutable from 'immutable'; +import { translate } from 'react-i18next'; +import PropTypes from 'prop-types'; +import interoperableErrors from '../../../shared/interoperable-errors'; +import { withPageHelpers } from './page' +import { withErrorHandling, withAsyncErrorHandler } from './error-handling'; +import { TreeTable, TreeSelectMode } from './tree'; + +const FormState = { + Loading: 0, + LoadingWithNotice: 1, + Ready: 2 +}; + +const FormSendMethod = { + PUT: 0, + POST: 1 +}; + +@translate() +@withPageHelpers +@withErrorHandling +class Form extends Component { + static propTypes = { + stateOwner: PropTypes.object.isRequired, + onSubmitAsync: PropTypes.func + } + + static childContextTypes = { + formStateOwner: PropTypes.object + } + + getChildContext() { + return { + formStateOwner: this.props.stateOwner + }; + } + + @withAsyncErrorHandler + async onSubmit(evt) { + const t = this.props.t; + + const owner = this.props.stateOwner; + + try { + evt.preventDefault(); + + if (this.props.onSubmitAsync) { + await this.props.onSubmitAsync(evt); + } + } catch (error) { + if (error instanceof interoperableErrors.ChangedError) { + owner.disableForm(); + owner.setFormStatusMessage('danger', + + {t('Your updates cannot be saved.')}{' '} + {t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')} + + ); + return; + } + + throw error; + } + } + + render() { + const t = this.props.t; + const owner = this.props.stateOwner; + const props = this.props; + const statusMessageText = owner.getFormStatusMessageText(); + const statusMessageSeverity = owner.getFormStatusMessageSeverity(); + + if (!owner.isFormReady()) { + if (owner.isFormWithLoadingNotice()) { + return

{t('Loading ...')}

+ } else { + return
; + } + } else { + return ( +
+
+ {props.children} +
+ {statusMessageText &&

{statusMessageText}

} +
+ ); + } + } +} + +function wrapInput(id, htmlId, owner, label, input) { + return ( +
+
+ +
+
+ {input} +
+
{owner.getFormValidationMessage(id)}
+
+ ); +} + +class InputField extends Component { + static propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + placeholder: PropTypes.string, + type: PropTypes.string + } + + static defaultProps = { + type: 'text' + } + + static contextTypes = { + formStateOwner: PropTypes.object.isRequired + } + + render() { + const props = this.props; + const owner = this.context.formStateOwner; + const id = this.props.id; + const htmlId = 'form_' + id; + + let type = 'text'; + if (props.type === 'password') { + type = 'password'; + } + + return wrapInput(id, htmlId, owner, props.label, + owner.updateFormValue(id, evt.target.value)}/> + ); + } +} + +class TextArea extends Component { + static propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + placeholder: PropTypes.string + } + + static contextTypes = { + formStateOwner: PropTypes.object.isRequired + } + + render() { + const props = this.props; + const owner = this.context.formStateOwner; + const id = this.props.id; + const htmlId = 'form_' + id; + + return wrapInput(id, htmlId, owner, props.label, + + ); + } +} + +class ButtonRow extends Component { + render() { + return ( +
+
+ {this.props.children} +
+
+ ); + } +} + +@withErrorHandling +class Button extends Component { + static propTypes = { + onClickAsync: PropTypes.func, + onClick: PropTypes.func, + label: PropTypes.string, + icon: PropTypes.string, + className: PropTypes.string, + type: PropTypes.string + } + + static contextTypes = { + formStateOwner: PropTypes.object.isRequired + } + + @withAsyncErrorHandler + async onClick(evt) { + if (this.props.onClick) { + evt.preventDefault(); + + onClick(evt); + + } else if (this.props.onClickAsync) { + evt.preventDefault(); + + this.context.formStateOwner.disableForm(); + await this.props.onClickAsync(evt); + this.context.formStateOwner.enableForm(); + } + } + + render() { + const props = this.props; + + let className = 'btn'; + if (props.className) { + className = className + ' ' + props.className; + } + + let type = props.type || 'button'; + + let icon; + if (props.icon) { + icon = + } + + let iconSpacer; + if (props.icon && props.label) { + iconSpacer = ' '; + } + + return ( + + ); + } +} + +class TreeTableSelect extends Component { + static propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + dataUrl: PropTypes.string, + data: PropTypes.array + } + + static contextTypes = { + formStateOwner: PropTypes.object.isRequired + } + + async onSelectionChangedAsync(sel) { + const owner = this.context.formStateOwner; + owner.updateFormValue(this.props.id, sel); + } + + render() { + const props = this.props; + const owner = this.context.formStateOwner; + const id = this.props.id; + const htmlId = 'form_' + id; + + return ( +
+
+ +
+
+ +
+
{owner.getFormValidationMessage(id)}
+
+ ); + } +} + +function withForm(target) { + const inst = target.prototype; + + const cleanFormState = Immutable.Map({ + state: FormState.Loading, + isValidationShown: false, + isDisabled: false, + statusMessageText: '', + data: Immutable.Map(), + isServerValidationRunning: false + }); + + inst.initForm = function(settings) { + const state = this.state || {}; + state.formState = cleanFormState; + state.formSettings = settings; + this.state = state; + }; + + inst.resetFormState = function() { + this.setState({ + formState: cleanFormState + }); + }; + + inst.getFormValuesFromURL = async function(url, mutator) { + setTimeout(() => { + this.setState(previousState => { + if (previousState.formState.get('state') === FormState.Loading) { + return { + formState: previousState.formState.set('state', FormState.LoadingWithNotice) + }; + } + }); + }, 500); + + const response = await axios.get(url); + + const data = response.data; + + data.originalHash = data.hash; + delete data.hash; + + if (mutator) { + mutator(data); + } + + this.populateFormValues(data); + }; + + inst.validateAndSendFormValuesToURL = async function(method, url, mutator) { + await this.waitForFormServerValidated(); + + if (this.isFormWithoutErrors()) { + const data = this.getFormValues(); + + if (mutator) { + mutator(data); + } + + if (method === FormSendMethod.PUT) { + await axios.put(url, data); + } else if (method === FormSendMethod.POST) { + await axios.post(url, data); + } + return true; + } else { + this.showFormValidation(); + return false; + } + }; + + + inst.populateFormValues = function(data) { + this.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + mutState.set('state', FormState.Ready); + + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + for (const key in data) { + mutStateData.set(key, Immutable.Map({ + value: data[key] + })); + } + })); + + this.validateForm(mutState); + }) + })); + }; + + + // formValidateResolve is called by "validateForm" once client receives validation response from server that does not + // trigger another server validation + let formValidateResolve = null; + + const scheduleValidateForm = (self) => { + setTimeout(() => { + self.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + self.validateForm(mutState); + }) + })); + }, 0); + }; + + inst.waitForFormServerValidated = async function() { + if (!this.isFormServerValidated()) { + await new Promise(resolve => { formValidateResolve = resolve; }); + } + }; + + inst.validateForm = function(mutState) { + const settings = this.state.formSettings; + + if (!mutState.get('isServerValidationRunning') && settings.serverValidation) { + const payload = {}; + let payloadNotEmpty = false; + + for (const attr of settings.serverValidation.extra || []) { + payload[attr] = mutState.getIn(['data', attr, 'value']); + } + + for (const attr of settings.serverValidation.changed) { + const currValue = mutState.getIn(['data', attr, 'value']); + const serverValue = mutState.getIn(['data', attr, 'serverValue']); + + // This really assumes that all form values are preinitialized (i.e. not undef) + if (currValue !== serverValue) { + mutState.setIn(['data', attr, 'serverValidated'], false); + payload[attr] = currValue; + payloadNotEmpty = true; + } + } + + if (payloadNotEmpty) { + mutState.set('isServerValidationRunning', true); + + axios.post(settings.serverValidation.url, payload) + .then(response => { + + this.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + mutState.set('isServerValidationRunning', false); + + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + for (const attr in payload) { + mutStateData.setIn([attr, 'serverValue'], payload[attr]); + + if (payload[attr] === mutState.getIn(['data', attr, 'value'])) { + mutStateData.setIn([attr, 'serverValidated'], true); + mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true); + } + } + })); + }) + })); + + scheduleValidateForm(this); + }) + .catch(error => { + console.log('Ignoring unhandled error in "validateForm": ' + error); + + this.setState(previousState => ({ + formState: previousState.formState.set('isServerValidationRunning', false) + })); + + scheduleValidateForm(this); + }); + } else { + if (formValidateResolve) { + const resolve = formValidateResolve; + formValidateResolve = null; + resolve(); + } + } + } + + if (this.localValidateFormValues) { + mutState.update('data', stateData => stateData.withMutations(mutStateData => { + this.localValidateFormValues(mutStateData); + })); + } + }; + + inst.updateFormValue = function(key, value) { + this.setState(previousState => ({ + formState: previousState.formState.withMutations(mutState => { + mutState.setIn(['data', key, 'value'], value); + this.validateForm(mutState); + }) + })); + }; + + inst.getFormValue = function(name) { + return this.state.formState.getIn(['data', name, 'value']); + }; + + inst.getFormValues = function(name) { + return this.state.formState.get('data').map(attr => attr.get('value')).toJS(); + }; + + inst.getFormError = function(name) { + return this.state.formState.getIn(['data', name, 'error']); + }; + + inst.isFormWithLoadingNotice = function() { + return this.state.formState.get('state') === FormState.LoadingWithNotice; + }; + + inst.isFormLoading = function() { + return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice; + }; + + inst.isFormReady = function() { + return this.state.formState.get('state') === FormState.Ready; + }; + + inst.isFormValidationShown = function() { + return this.state.formState.get('isValidationShown'); + }; + + inst.addFormValidationClass = function(className, name) { + if (this.isFormValidationShown()) { + const error = this.getFormError(name); + if (error) { + return className + ' has-error'; + } else { + return className + ' has-success'; + } + } else { + return className; + } + }; + + inst.getFormValidationMessage = function(name) { + if (this.isFormValidationShown()) { + return this.getFormError(name); + } else { + return ''; + } + }; + + inst.showFormValidation = function() { + this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)})); + }; + + inst.hideFormValidation = function() { + this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)})); + }; + + inst.isFormWithoutErrors = function() { + return !this.state.formState.get('data').find(attr => attr.get('error')); + }; + + inst.isFormServerValidated = function() { + return this.state.formSettings.serverValidation.changed.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated'])); + }; + + inst.getFormStatusMessageText = function() { + return this.state.formState.get('statusMessageText'); + }; + + inst.getFormStatusMessageSeverity = function() { + return this.state.formState.get('statusMessageSeverity'); + }; + + inst.setFormStatusMessage = function(severity, text) { + this.setState(previousState => ({ + formState: previousState.formState.withMutations(map => { + map.set('statusMessageText', text); + map.set('statusMessageSeverity', severity); + }) + })); + }; + + inst.enableForm = function() { + this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)})); + }; + + inst.disableForm = function() { + this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)})); + }; + + inst.isFormDisabled = function() { + return this.state.formState.get('isDisabled'); + }; + + return target; +} + + +export { + withForm, + Form, + InputField, + TextArea, + ButtonRow, + Button, + TreeTableSelect, + FormSendMethod +} diff --git a/client/src/lib/page.css b/client/src/lib/page.css index d67bd1f3..2e996132 100644 --- a/client/src/lib/page.css +++ b/client/src/lib/page.css @@ -1,20 +1,24 @@ -.mt-button-row > button { - margin-right: 15px; -} - -.mt-button-row > button:last-child { - margin-right: 0px; -} - -.mt-form-status { - padding-top: 5px; - padding-bottom: 5px; -} - -.mt-action-links > a { - margin-right: 8px; -} - -.mt-action-links > a:last-child { - margin-right: 0px; -} +.mt-button-row > button { + margin-right: 15px; +} + +.mt-button-row > button:last-child { + margin-right: 0px; +} + +.mt-form-status { + padding-top: 5px; + padding-bottom: 5px; +} + +.mt-action-links > a { + margin-right: 8px; +} + +.mt-action-links > a:last-child { + margin-right: 0px; +} + +.form-horizontal .control-label { + display: block; +} \ No newline at end of file diff --git a/client/src/namespaces/CUD.js b/client/src/namespaces/CUD.js index 986d5117..69b8dd9e 100644 --- a/client/src/namespaces/CUD.js +++ b/client/src/namespaces/CUD.js @@ -23,7 +23,7 @@ export default class CUD extends Component { this.state.entityId = parseInt(props.match.params.id); } - this.initFormState(); + this.initForm(); this.hasChildren = false; } diff --git a/client/src/users/CUD.js b/client/src/users/CUD.js index 8ac8c07d..49f7a682 100644 --- a/client/src/users/CUD.js +++ b/client/src/users/CUD.js @@ -8,6 +8,7 @@ import axios from '../lib/axios'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import interoperableErrors from '../../../shared/interoperable-errors'; import passwordValidator from '../../../shared/password-validator'; +import validators from '../../../shared/validators'; import { ModalDialog } from '../lib/bootstrap-components'; @translate() @@ -26,7 +27,13 @@ export default class CUD extends Component { this.state.entityId = parseInt(props.match.params.id); } - this.initFormState('/users/rest/validate', ['username', 'email']); + this.initForm({ + serverValidation: { + url: '/users/rest/validate', + changed: ['username', 'email'], + extra: ['id'] + } + }); this.hasChildren = false; } @@ -36,7 +43,10 @@ export default class CUD extends Component { @withAsyncErrorHandler async loadFormValues() { - await this.getFormValuesFromURL(`/users/rest/users/${this.state.entityId}`); + await this.getFormValuesFromURL(`/users/rest/users/${this.state.entityId}`, data => { + data.password = ''; + data.password2 = ''; + }); } componentDidMount() { @@ -46,7 +56,9 @@ export default class CUD extends Component { this.populateFormValues({ username: '', name: '', - email: '' + email: '', + password: '', + password2: '' }); } } @@ -57,12 +69,11 @@ export default class CUD extends Component { const username = state.getIn(['username', 'value']); - const usernamePattern = /^[a-zA-Z0-9][a-zA-Z0-9_\-.]*$/; const usernameServerValidation = state.getIn(['username', 'serverValidation']); if (!username) { state.setIn(['username', 'error'], t('User name must not be empty')); - } else if (!usernamePattern.test(username)) { + } else if (!validators.usernameValid(username)) { state.setIn(['username', 'error'], t('User name may contain only the following characters: A-Z, a-z, 0-9, "_", "-", "." and may start only with A-Z, a-z, 0-9.')); } else if (!usernameServerValidation || usernameServerValidation.exists) { state.setIn(['username', 'error'], t('The user name already exists in the system.')); @@ -132,7 +143,9 @@ export default class CUD extends Component { this.disableForm(); this.setFormStatusMessage('info', t('Saving user ...')); - const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url); + const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { + delete data.password2; + }); if (submitSuccessful) { this.navigateToWithFlashMessage('/users', 'success', t('User saved')); @@ -179,10 +192,11 @@ export default class CUD extends Component { render() { const t = this.props.t; const edit = this.props.edit; + const isAdmin = this.getFormValue('id') === 1; return (
- {edit && + {edit && !isAdmin &&