rework org edit form to use Formik

This commit is contained in:
Keith Grant
2019-03-27 10:42:06 -04:00
parent 02cd188c2f
commit 40b2539626
6 changed files with 292 additions and 175 deletions

View File

@@ -16,7 +16,7 @@ class AnsibleSelect extends React.Component {
onSelectChange (val, event) {
const { onChange, name } = this.props;
event.target.name = name;
onChange(val, event);
onChange(event, val);
}
render () {
@@ -24,10 +24,22 @@ class AnsibleSelect extends React.Component {
return (
<I18n>
{({ i18n }) => (
<FormSelect value={value} onChange={this.onSelectChange} aria-label={i18n._(t`Select Input`)}>
{data.map((datum) => (datum === defaultSelected
? (<FormSelectOption key="" value="" label={i18n._(t`Use Default ${label}`)} />) : (<FormSelectOption key={datum} value={datum} label={datum} />)))
}
<FormSelect
value={value}
onChange={this.onSelectChange}
aria-label={i18n._(t`Select Input`)}
>
{data.map((datum) => (
datum === defaultSelected ? (
<FormSelectOption
key=""
value=""
label={i18n._(t`Use Default ${label}`)}
/>
) : (
<FormSelectOption key={datum} value={datum} label={datum} />
)
))}
</FormSelect>
)}
</I18n>

View File

@@ -28,10 +28,10 @@ const FormActionGroup = ({ onSubmit, submitDisabled, onCancel }) => (
<ActionGroup style={formActionGroupStyle}>
<Toolbar>
<ToolbarGroup style={buttonGroupStyle}>
<Button aria-label={i18n._(t`Save`)} variant="primary" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
<Button aria-label={i18n._(t`Save`)} variant="primary" type="submit" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
</ToolbarGroup>
<ToolbarGroup>
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" type="button" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
</ToolbarGroup>
</Toolbar>
</ActionGroup>

View File

@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field } from 'formik';
import { FormGroup, TextInput } from '@patternfly/react-core';
function FormField (props) {
const { id, name, label, validate, isRequired, ...rest } = props;
return (
<Field
name={name}
validate={validate}
render={({ field, form }) => {
const isValid = !form.touched[field.name] || !form.errors[field.name];
return (
<FormGroup
fieldId={id}
helperTextInvalid={form.errors[field.name]}
isRequired={isRequired}
isValid={isValid}
label={label}
>
<TextInput
id={id}
isRequired={isRequired}
isValid={isValid}
{...rest}
{...field}
onChange={(value, event) => {
field.onChange(event);
}}
/>
</FormGroup>
);
}}
/>
);
}
FormField.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
type: PropTypes.string,
validate: PropTypes.func,
isRequired: PropTypes.bool,
};
FormField.defaultProps = {
type: 'text',
validate: () => {},
isRequired: false,
};
export default FormField;

View File

@@ -1,27 +1,35 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik';
import { I18n, i18nMark } from '@lingui/react';
import { t } from '@lingui/macro';
import {
CardBody,
Form,
FormGroup,
TextInput,
} from '@patternfly/react-core';
import { ConfigContext } from '../../../../context';
import FormField from '../../../../components/FormField';
import FormActionGroup from '../../../../components/FormActionGroup';
import AnsibleSelect from '../../../../components/AnsibleSelect';
import InstanceGroupsLookup from '../../components/InstanceGroupsLookup';
function required (message) {
return value => {
if (!value.trim()) {
return message || i18nMark('This field must not be blank');
}
return undefined;
};
}
class OrganizationEdit extends Component {
constructor (props) {
super(props);
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
this.checkValidity = this.checkValidity.bind(this);
this.handleFieldChange = this.handleFieldChange.bind(this);
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postInstanceGroups = this.postInstanceGroups.bind(this);
@@ -29,48 +37,25 @@ class OrganizationEdit extends Component {
this.handleSuccess = this.handleSuccess.bind(this);
this.state = {
form: {
name: {
value: '',
isValid: true,
validation: {
required: true
},
helperTextInvalid: i18nMark('This field must not be blank')
},
description: {
value: ''
},
instanceGroups: {
value: [],
initialValue: []
},
custom_virtualenv: {
value: '',
defaultValue: '/venv/ansible/'
}
},
initialInstanceGroups: [],
instanceGroups: [],
error: '',
formIsValid: true
formIsValid: true,
};
}
async componentDidMount () {
const { organization } = this.props;
const { form: formData } = this.state;
formData.name.value = organization.name;
formData.description.value = organization.description;
formData.custom_virtualenv.value = organization.custom_virtualenv;
let instanceGroups;
try {
formData.instanceGroups.value = await this.getRelatedInstanceGroups();
formData.instanceGroups.initialValue = [...formData.instanceGroups.value];
instanceGroups = await this.getRelatedInstanceGroups();
} catch (err) {
this.setState({ error: err });
}
this.setState({ form: formData });
this.setState({
instanceGroups,
initialInstanceGroups: instanceGroups,
});
}
async getRelatedInstanceGroups () {
@@ -79,56 +64,19 @@ class OrganizationEdit extends Component {
organization: { id }
} = this.props;
const { data } = await api.getOrganizationInstanceGroups(id);
const { results } = data;
return results;
return data.results;
}
checkValidity = (value, validation) => {
const isValid = (validation.required)
? (value.trim() !== '') : true;
return isValid;
handleInstanceGroupsChange (instanceGroups) {
this.setState({ instanceGroups });
}
handleFieldChange (val, evt) {
const targetName = evt.target.name;
const value = val;
const { form: updatedForm } = this.state;
const updatedFormEl = { ...updatedForm[targetName] };
updatedFormEl.value = value;
updatedForm[targetName] = updatedFormEl;
updatedFormEl.isValid = (updatedFormEl.validation)
? this.checkValidity(updatedFormEl.value, updatedFormEl.validation) : true;
const formIsValid = (updatedFormEl.validation) ? updatedFormEl.isValid : true;
this.setState({ form: updatedForm, formIsValid });
}
handleInstanceGroupsChange (val, targetName) {
const { form: updatedForm } = this.state;
updatedForm[targetName].value = val;
this.setState({ form: updatedForm });
}
async handleSubmit () {
async handleSubmit (values) {
const { api, organization } = this.props;
const { form: { name, description, custom_virtualenv } } = this.state;
const formData = { name, description, custom_virtualenv };
const updatedData = {};
Object.keys(formData)
.forEach(formId => {
updatedData[formId] = formData[formId].value;
});
const { instanceGroups } = this.state;
try {
await api.updateOrganizationDetails(organization.id, updatedData);
await this.postInstanceGroups();
await api.updateOrganizationDetails(organization.id, values);
await this.postInstanceGroups(instanceGroups);
} catch (err) {
this.setState({ error: err });
} finally {
@@ -146,18 +94,18 @@ class OrganizationEdit extends Component {
history.push(`/organizations/${id}`);
}
async postInstanceGroups () {
async postInstanceGroups (instanceGroups) {
const { api, organization } = this.props;
const { form: { instanceGroups } } = this.state;
const { initialInstanceGroups } = this.state;
const url = organization.related.instance_groups;
const initialInstanceGroups = instanceGroups.initialValue.map(ig => ig.id);
const updatedInstanceGroups = instanceGroups.value.map(ig => ig.id);
const initialIds = initialInstanceGroups.map(ig => ig.id);
const updatedIds = instanceGroups.map(ig => ig.id);
const groupsToAssociate = [...updatedInstanceGroups]
.filter(x => !initialInstanceGroups.includes(x));
const groupsToDisassociate = [...initialInstanceGroups]
.filter(x => !updatedInstanceGroups.includes(x));
const groupsToAssociate = [...updatedIds]
.filter(x => !initialIds.includes(x));
const groupsToDisassociate = [...initialIds]
.filter(x => !updatedIds.includes(x));
try {
await Promise.all(groupsToAssociate.map(async id => {
@@ -172,83 +120,85 @@ class OrganizationEdit extends Component {
}
render () {
const { api } = this.props;
const { api, organization } = this.props;
const {
form: {
name,
description,
instanceGroups,
custom_virtualenv
},
instanceGroups,
formIsValid,
error
error,
} = this.state;
const defaultVenv = '/venv/ansible/';
return (
<CardBody>
<I18n>
{({ i18n }) => (
<Form autoComplete="off">
<div style={{ display: 'grid', gridGap: '20px', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))' }}>
<FormGroup
fieldId="edit-org-form-name"
helperTextInvalid={name.helperTextInvalid}
isRequired
isValid={name.isValid}
label={i18n._(t`Name`)}
>
<TextInput
id="edit-org-form-name"
isRequired
isValid={name.isValid}
name="name"
onChange={this.handleFieldChange}
value={name.value || ''}
<Formik
initialValues={{
name: organization.name,
description: organization.description,
custom_virtualenv: organization.custom_virtualenv || '',
}}
onSubmit={this.handleSubmit}
render={formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<div
style={{
display: 'grid',
gridGap: '20px',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))'
}}
>
<FormField
id="edit-org-form-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required()}
isRequired
/>
<FormField
id="edit-org-form-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<Field
name="custom_virtualenv"
render={({ field }) => (
<FormGroup
fieldId="edit-org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
data={custom_virtualenvs}
defaultSelected={defaultVenv}
label={i18n._(t`Ansible Environment`)}
{...field}
/>
</FormGroup>
)}
/>
)
)}
</ConfigContext.Consumer>
</div>
<InstanceGroupsLookup
api={api}
value={instanceGroups}
onChange={this.handleInstanceGroupsChange}
/>
</FormGroup>
<FormGroup
fieldId="edit-org-form-description"
label={i18n._(t`Description`)}
>
<TextInput
id="edit-org-form-description"
name="description"
onChange={this.handleFieldChange}
value={description.value || ''}
<FormActionGroup
onCancel={this.handleCancel}
onSubmit={formik.handleSubmit}
submitDisabled={!formIsValid}
/>
</FormGroup>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<FormGroup
fieldId="edit-org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
data={custom_virtualenvs}
defaultSelected={custom_virtualenv.defaultEnv}
label={i18n._(t`Ansible Environment`)}
name="custom_virtualenv"
onChange={this.handleFieldChange}
value={custom_virtualenv.value || ''}
/>
</FormGroup>
)
)}
</ConfigContext.Consumer>
</div>
<InstanceGroupsLookup
api={api}
value={instanceGroups.value}
onChange={this.handleInstanceGroupsChange}
/>
<FormActionGroup
onCancel={this.handleCancel}
onSubmit={this.handleSubmit}
submitDisabled={!formIsValid}
/>
{ error ? <div>error</div> : '' }
</Form>
{ error ? <div>error</div> : '' }
</Form>
)}
/>
)}
</I18n>
</CardBody>