Added support for Datatables
Added support for ajax-based server side validation (useful for validation of emails, duplicate usernames, etc.) User form more or less ready in the basic version (i.e. without permission management)
This commit is contained in:
parent
f776170854
commit
c81f5544e6
26 changed files with 1097 additions and 167 deletions
212
client/src/users/CUD.js
Normal file
212
client/src/users/CUD.js
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import { withForm, Form, FormSendMethod, InputField, TextArea, ButtonRow, Button, TreeTableSelect } from '../lib/form';
|
||||
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 { ModalDialog } from '../lib/bootstrap-components';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class CUD extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.passwordValidator = passwordValidator(props.t);
|
||||
|
||||
this.state = {};
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.id);
|
||||
}
|
||||
|
||||
this.initFormState('/users/rest/validate', ['username', 'email']);
|
||||
this.hasChildren = false;
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
return this.props.match.params.action === 'delete';
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/users/rest/users/${this.state.entityId}`);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
username: '',
|
||||
name: '',
|
||||
email: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
|
||||
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)) {
|
||||
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.'));
|
||||
} else {
|
||||
state.setIn(['username', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
const email = state.getIn(['email', 'value']);
|
||||
const emailServerValidation = state.getIn(['email', 'serverValidation']);
|
||||
|
||||
if (!email) {
|
||||
state.setIn(['email', 'error'], t('Email must not be empty'));
|
||||
} else if (!emailServerValidation || emailServerValidation.invalid) {
|
||||
state.setIn(['email', 'error'], t('Invalid email address.'));
|
||||
} else {
|
||||
state.setIn(['email', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
const name = state.getIn(['name', 'value']);
|
||||
|
||||
if (!name) {
|
||||
state.setIn(['name', 'error'], t('Full name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
const password = state.getIn(['password', 'value']) || '';
|
||||
const password2 = state.getIn(['password2', 'value']) || '';
|
||||
|
||||
const passwordResults = this.passwordValidator.test(password);
|
||||
|
||||
let passwordMsgs = [];
|
||||
|
||||
if (!edit && !password) {
|
||||
passwordMsgs.push(t('Password must not be empty'));
|
||||
}
|
||||
|
||||
if (password) {
|
||||
passwordMsgs.push(...passwordResults.errors);
|
||||
}
|
||||
|
||||
if (passwordMsgs.length > 1) {
|
||||
passwordMsgs = passwordMsgs.map((msg, idx) => <div key={idx}>{msg}</div>)
|
||||
}
|
||||
|
||||
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
|
||||
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
let sendMethod, url;
|
||||
if (edit) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/users/rest/users/${this.state.entityId}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/users/rest/users'
|
||||
}
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving user ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage('/users', 'success', t('User 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.LoopDetectedError) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('The username is already assigned to another user.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async showDeleteModal() {
|
||||
this.navigateTo(`/users/edit/${this.state.entityId}/delete`);
|
||||
}
|
||||
|
||||
async hideDeleteModal() {
|
||||
this.navigateTo(`/users/edit/${this.state.entityId}`);
|
||||
}
|
||||
|
||||
async performDelete() {
|
||||
const t = this.props.t;
|
||||
|
||||
await this.hideDeleteModal();
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting user...'));
|
||||
|
||||
await axios.delete(`/users/rest/users/${this.state.entityId}`);
|
||||
|
||||
this.navigateToWithFlashMessage('/users', 'success', t('User deleted'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
<ModalDialog hidden={!this.isDelete()} title={t('Confirm deletion')} onCloseAsync={::this.hideDeleteModal} buttons={[
|
||||
{ label: t('No'), className: 'btn-primary', onClickAsync: ::this.hideDeleteModal },
|
||||
{ label: t('Yes'), className: 'btn-danger', onClickAsync: ::this.performDelete }
|
||||
]}>
|
||||
{t('Are you sure you want to delete user "{{username}}"?', {username: this.getFormValue('username')})}
|
||||
</ModalDialog>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit User') : t('Create User')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="username" label={t('User Name')}/>
|
||||
<InputField id="name" label={t('Full Name')}/>
|
||||
<InputField id="email" label={t('Email')}/>
|
||||
<InputField id="password" label={t('Password')}/>
|
||||
<InputField id="password2" label={t('Repeat Password')}/>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <Button className="btn-danger" icon="remove" label={t('Delete User')}
|
||||
onClickAsync={::this.showDeleteModal}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
38
client/src/users/List.js
Normal file
38
client/src/users/List.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { Table, TableSelectMode } from '../lib/table';
|
||||
|
||||
@translate()
|
||||
export default class List extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actionLinks = [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: data => '/users/edit/' + data[0]
|
||||
}
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: "Username" },
|
||||
{ data: 2, title: "Full Name" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<NavButton linkTo="/users/create" className="btn-primary" icon="plus" label={t('Create User')}/>
|
||||
</Toolbar>
|
||||
|
||||
<Title>{t('Users')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/users/rest/usersTable" columns={columns} actionLinks={actionLinks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
44
client/src/users/root.js
Normal file
44
client/src/users/root.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page'
|
||||
import CUD from './CUD'
|
||||
import List from './List'
|
||||
|
||||
const getStructure = t => ({
|
||||
'': {
|
||||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'users': {
|
||||
title: t('Users'),
|
||||
link: '/users',
|
||||
component: List,
|
||||
children: {
|
||||
'edit' : {
|
||||
title: t('Edit User'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
},
|
||||
'create' : {
|
||||
title: t('Create User'),
|
||||
render: props => (<CUD {...props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={ i18n }><Section root='/users' structure={getStructure}/></I18nextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue