update forms to useField fomik hook

This commit is contained in:
John Mitchell
2020-02-18 14:30:59 -05:00
parent a42ff9865b
commit ff823c9fdb
14 changed files with 692 additions and 723 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { string, bool } from 'prop-types'; import { string, bool } from 'prop-types';
import { Field, useFormikContext } from 'formik'; import { useField } from 'formik';
import { Split, SplitItem } from '@patternfly/react-core'; import { Split, SplitItem } from '@patternfly/react-core';
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml'; import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
import CodeMirrorInput from './CodeMirrorInput'; import CodeMirrorInput from './CodeMirrorInput';
@@ -8,9 +8,8 @@ import YamlJsonToggle from './YamlJsonToggle';
import { JSON_MODE, YAML_MODE } from './constants'; import { JSON_MODE, YAML_MODE } from './constants';
function VariablesField({ id, name, label, readOnly }) { function VariablesField({ id, name, label, readOnly }) {
const { values } = useFormikContext(); const [field, meta, helpers] = useField(name);
const value = values[name]; const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE);
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
return ( return (
<Field name={name}> <Field name={name}>
@@ -31,10 +30,10 @@ function VariablesField({ id, name, label, readOnly }) {
newMode === YAML_MODE newMode === YAML_MODE
? jsonToYaml(field.value) ? jsonToYaml(field.value)
: yamlToJson(field.value); : yamlToJson(field.value);
form.setFieldValue(name, newVal); helpers.setValue(newVal);
setMode(newMode); setMode(newMode);
} catch (err) { } catch (err) {
form.setFieldError(name, err.message); helpers.setError(err.message);
} }
}} }}
/> />
@@ -45,16 +44,13 @@ function VariablesField({ id, name, label, readOnly }) {
readOnly={readOnly} readOnly={readOnly}
{...field} {...field}
onChange={newVal => { onChange={newVal => {
form.setFieldValue(name, newVal); helpers.setValue(newVal);
}} }}
hasErrors={!!form.errors[field.name]} hasErrors={!!meta.error}
/> />
{form.errors[field.name] ? ( {meta.error ? (
<div <div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
className="pf-c-form__helper-text pf-m-error" {meta.error}
aria-live="polite"
>
{form.errors[field.name]}
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { string, func } from 'prop-types'; import { string, func } from 'prop-types';
import { Field } from 'formik'; import { useField } from 'formik';
import { Checkbox, Tooltip } from '@patternfly/react-core'; import { Checkbox, Tooltip } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -10,32 +10,29 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
`; `;
function CheckboxField({ id, name, label, tooltip, validate, ...rest }) { function CheckboxField({ id, name, label, tooltip, validate, ...rest }) {
const [field] = useField({ name, validate });
return ( return (
<Field name={name} validate={validate}> <Checkbox
{({ field }) => ( aria-label={label}
<Checkbox label={
aria-label={label} <span>
label={ {label}
<span> &nbsp;
{label} {tooltip && (
&nbsp; <Tooltip position="right" content={tooltip}>
{tooltip && ( <QuestionCircleIcon />
<Tooltip position="right" content={tooltip}> </Tooltip>
<QuestionCircleIcon /> )}
</Tooltip> </span>
)} }
</span> id={id}
} {...rest}
id={id} isChecked={field.value}
{...rest} {...field}
isChecked={field.value} onChange={(value, event) => {
{...field} field.onChange(event);
onChange={(value, event) => { }}
field.onChange(event); />
}}
/>
)}
</Field>
); );
} }
CheckboxField.propTypes = { CheckboxField.propTypes = {

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field } from 'formik'; import { useField } from 'formik';
import { FormGroup, TextInput, Tooltip } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -21,16 +20,13 @@ function FormField(props) {
...rest ...rest
} = props; } = props;
return ( const [field, meta] = useField({ name, validate });
<Field name={name} validate={validate}> const isValid = !(meta.touched && meta.error);
{({ field, form }) => {
const isValid =
form && (!form.touched[field.name] || !form.errors[field.name]);
return ( return (
<FormGroup <FormGroup
fieldId={id} fieldId={id}
helperTextInvalid={form.errors[field.name]} helperTextInvalid={meta.error}
isRequired={isRequired} isRequired={isRequired}
isValid={isValid} isValid={isValid}
label={label} label={label}
@@ -44,6 +40,8 @@ function FormField(props) {
<QuestionCircleIcon /> <QuestionCircleIcon />
</Tooltip> </Tooltip>
)} )}
isValid={isValid}
helperTextInvalid={meta.error}
<TextInput <TextInput
id={id} id={id}
isRequired={isRequired} isRequired={isRequired}

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Field } from 'formik'; import { useField } from 'formik';
import { import {
Button, Button,
ButtonVariant, ButtonVariant,
@@ -16,54 +16,46 @@ import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
function PasswordField(props) { function PasswordField(props) {
const { id, name, label, validate, isRequired, i18n } = props; const { id, name, label, validate, isRequired, i18n } = props;
const [inputType, setInputType] = useState('password'); const [inputType, setInputType] = useState('password');
const [field, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const handlePasswordToggle = () => { const handlePasswordToggle = () => {
setInputType(inputType === 'text' ? 'password' : 'text'); setInputType(inputType === 'text' ? 'password' : 'text');
}; };
return ( return (
<Field name={name} validate={validate}> <FormGroup
{({ field, form }) => { fieldId={id}
const isValid = helperTextInvalid={meta.error}
form && (!form.touched[field.name] || !form.errors[field.name]); isRequired={isRequired}
return ( isValid={isValid}
<FormGroup label={label}
fieldId={id} >
helperTextInvalid={form.errors[field.name]} <InputGroup>
isRequired={isRequired} <Tooltip
isValid={isValid} content={inputType === 'password' ? i18n._(t`Show`) : i18n._(t`Hide`)}
label={label} >
<Button
variant={ButtonVariant.control}
aria-label={i18n._(t`Toggle Password`)}
onClick={handlePasswordToggle}
> >
<InputGroup> {inputType === 'password' && <EyeSlashIcon />}
<Tooltip {inputType === 'text' && <EyeIcon />}
content={ </Button>
inputType === 'password' ? i18n._(t`Show`) : i18n._(t`Hide`) </Tooltip>
} <TextInput
> id={id}
<Button isRequired={isRequired}
variant={ButtonVariant.control} isValid={isValid}
aria-label={i18n._(t`Toggle Password`)} type={inputType}
onClick={handlePasswordToggle} {...field}
> onChange={(value, event) => {
{inputType === 'password' && <EyeSlashIcon />} field.onChange(event);
{inputType === 'text' && <EyeIcon />} }}
</Button> />
</Tooltip> </InputGroup>
<TextInput </FormGroup>
id={id}
isRequired={isRequired}
isValid={isValid}
type={inputType}
{...field}
onChange={(value, event) => {
field.onChange(event);
}}
/>
</InputGroup>
</FormGroup>
);
}}
</Field>
); );
} }

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { func, shape } from 'prop-types'; import { func, shape } from 'prop-types';
import { useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import { Formik, Field } from 'formik'; import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -15,26 +15,21 @@ import { VariablesField } from '@components/CodeMirrorInput';
import { required } from '@util/validators'; import { required } from '@util/validators';
import { InventoryLookup } from '@components/Lookup'; import { InventoryLookup } from '@components/Lookup';
function HostForm({ handleSubmit, handleCancel, host, submitError, i18n }) { function HostFormFields({ host, i18n }) {
const [inventory, setInventory] = useState( const [inventory, setInventory] = useState(
host ? host.summary_fields.inventory : '' host ? host.summary_fields.inventory : ''
); );
const hostAddMatch = useRouteMatch('/hosts/add'); const hostAddMatch = useRouteMatch('/hosts/add');
const inventoryFieldArr = useField({
name: 'inventory',
validate: required(i18n._(t`Select aå value for this field`), i18n),
});
const inventoryMeta = inventoryFieldArr[1];
const inventoryHelpers = inventoryFieldArr[2];
return ( return (
<Formik <>
initialValues={{
name: host.name,
description: host.description,
inventory: host.inventory || '',
variables: host.variables,
}}
onSubmit={handleSubmit}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<FormField <FormField
id="host-name" id="host-name"
name="name" name="name"
@@ -50,41 +45,46 @@ function HostForm({ handleSubmit, handleCancel, host, submitError, i18n }) {
label={i18n._(t`Description`)} label={i18n._(t`Description`)}
/> />
{hostAddMatch && ( {hostAddMatch && (
<Field
name="inventory"
validate={required(
i18n._(t`Select a value for this field`),
i18n
)}
>
{({ form }) => (
<InventoryLookup <InventoryLookup
value={inventory} value={inventory}
onBlur={() => form.setFieldTouched('inventory')} onBlur={() => inventoryHelpers.setTouched()}
tooltip={i18n._( tooltip={i18n._(
t`Select the inventory that this host will belong to.` t`Select the inventory that this host will belong to.`
)} )}
isValid={!form.touched.inventory || !form.errors.inventory} isValid={!inventoryMeta.touched || !inventoryMeta.error}
helperTextInvalid={form.errors.inventory} helperTextInvalid={inventoryMeta.error}
onChange={value => { onChange={value => {
form.setFieldValue('inventory', value.id); inventoryHelpers.setValuealue(value.id);
setInventory(value); setInventory(value);
}} }}
required required
touched={form.touched.inventory} touched={inventoryMeta.touched}
error={form.errors.inventory} error={inventoryMeta.error}
/> />
)} )}
</Field>
)}
</FormRow>
<FormRow>
<VariablesField <VariablesField
id="host-variables" id="host-variables"
name="variables" name="variables"
label={i18n._(t`Variables`)} label={i18n._(t`Variables`)}
/> />
</FormRow> </>
);
}
function HostForm({ handleSubmit, host, submitError, handleCancel, ...rest }) {
return (
<Formik
initialValues={{
name: host.name,
description: host.description,
inventory: host.inventory || '',
variables: host.variables,
}}
onSubmit={handleSubmit}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<HostFormFields host={host} {...rest} />
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Formik, Field } from 'formik'; import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { func, number, shape } from 'prop-types'; import { func, number, shape } from 'prop-types';
@@ -8,44 +8,25 @@ import { VariablesField } from '@components/CodeMirrorInput';
import { Form } from '@patternfly/react-core'; import { Form } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '@components/FormField'; import FormField, { FormSubmitError } from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormRow from '@components/FormRow';
import { required } from '@util/validators'; import { required } from '@util/validators';
import InstanceGroupsLookup from '@components/Lookup/InstanceGroupsLookup'; import InstanceGroupsLookup from '@components/Lookup/InstanceGroupsLookup';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import CredentialLookup from '@components/Lookup/CredentialLookup'; import CredentialLookup from '@components/Lookup/CredentialLookup';
function InventoryForm({ function InventoryFormFields({ i18n, credentialTypeId }) {
inventory = {}, const [organizationField, organizationMeta, organizationHelpers] = useField({
i18n, name: 'organization',
onCancel, validate: required(i18n._(t`Select a value for this field`), i18n),
onSubmit, });
instanceGroups, const instanceGroupsFieldArr = useField('instanceGroups');
credentialTypeId, const instanceGroupsField = instanceGroupsFieldArr[0];
submitError, const instanceGroupsHelpers = instanceGroupsFieldArr[2];
}) {
const initialValues = { const insightsCredentialFieldArr = useField('insights_credential');
name: inventory.name || '', const insightsCredentialField = insightsCredentialFieldArr[0];
description: inventory.description || '', const insightsCredentialHelpers = insightsCredentialFieldArr[2];
variables: inventory.variables || '---',
organization:
(inventory.summary_fields && inventory.summary_fields.organization) ||
null,
instanceGroups: instanceGroups || [],
insights_credential:
(inventory.summary_fields &&
inventory.summary_fields.insights_credential) ||
null,
};
return ( return (
<Formik <>
initialValues={initialValues}
onSubmit={values => {
onSubmit(values);
}}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<FormField <FormField
id="inventory-name" id="inventory-name"
label={i18n._(t`Name`)} label={i18n._(t`Name`)}
@@ -60,66 +41,30 @@ function InventoryForm({
name="description" name="description"
type="text" type="text"
/> />
<Field
id="inventory-organization"
label={i18n._(t`Organization`)}
name="organization"
validate={required(
i18n._(t`Select a value for this field`),
i18n
)}
>
{({ form, field }) => (
<OrganizationLookup <OrganizationLookup
helperTextInvalid={form.errors.organization} helperTextInvalid={organizationMeta.error}
isValid={ isValid={!organizationMeta.touched || !organizationMeta.error}
!form.touched.organization || !form.errors.organization onBlur={() => organizationHelpers.setTouched()}
}
onBlur={() => form.setFieldTouched('organization')}
onChange={value => { onChange={value => {
form.setFieldValue('organization', value); organizationHelpers.setValue(value);
}} }}
value={field.value} value={organizationField.value}
touched={form.touched.organization} touched={organizationMeta.touched}
error={form.errors.organization} error={organizationMeta.error}
required required
/> />
)}
</Field>
<Field
id="inventory-insights_credential"
label={i18n._(t`Insights Credential`)}
name="insights_credential"
>
{({ field, form }) => (
<CredentialLookup <CredentialLookup
label={i18n._(t`Insights Credential`)} label={i18n._(t`Insights Credential`)}
credentialTypeId={credentialTypeId} credentialTypeId={credentialTypeId}
onChange={value => onChange={value => insightsCredentialHelpers.setValue(value)}
form.setFieldValue('insights_credential', value) value={insightsCredentialField.value}
}
value={field.value}
/> />
)}
</Field>
</FormRow>
<FormRow>
<Field
id="inventory-instanceGroups"
label={i18n._(t`Instance Groups`)}
name="instanceGroups"
>
{({ field, form }) => (
<InstanceGroupsLookup <InstanceGroupsLookup
value={field.value} value={instanceGroupsField.value}
onChange={value => { onChange={value => {
form.setFieldValue('instanceGroups', value); instanceGroupsHelpers.setValue(value);
}} }}
/> />
)}
</Field>
</FormRow>
<FormRow>
<VariablesField <VariablesField
tooltip={i18n._( tooltip={i18n._(
t`Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax` t`Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax`
@@ -128,8 +73,42 @@ function InventoryForm({
name="variables" name="variables"
label={i18n._(t`Variables`)} label={i18n._(t`Variables`)}
/> />
</FormRow> </>
<FormRow> );
}
function InventoryForm({
inventory = {},
onSubmit,
onCancel,
submitError,
instanceGroups,
...rest
}) {
const initialValues = {
name: inventory.name || '',
description: inventory.description || '',
variables: inventory.variables || '---',
organization:
(inventory.summary_fields && inventory.summary_fields.organization) ||
null,
instanceGroups: instanceGroups || [],
insights_credential:
(inventory.summary_fields &&
inventory.summary_fields.insights_credential) ||
null,
};
return (
<Formik
initialValues={initialValues}
onSubmit={values => {
onSubmit(values);
}}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<InventoryFormFields {...rest} />
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={onCancel} onCancel={onCancel}

View File

@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik'; import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form, FormGroup } from '@patternfly/react-core';
@@ -18,20 +18,86 @@ import { InstanceGroupsLookup } from '@components/Lookup/';
import { getAddedAndRemoved } from '@util/lists'; import { getAddedAndRemoved } from '@util/lists';
import { required, minMaxValue } from '@util/validators'; import { required, minMaxValue } from '@util/validators';
function OrganizationForm({ function OrganizationFormFields({
organization,
i18n, i18n,
me, me,
onCancel, instanceGroups,
onSubmit, setInstanceGroups,
submitError,
}) { }) {
const [venvField] = useField('custom_virtualenv');
const defaultVenv = { const defaultVenv = {
label: i18n._(t`Use Default Ansible Environment`), label: i18n._(t`Use Default Ansible Environment`),
value: '/venv/ansible/', value: '/venv/ansible/',
key: 'default', key: 'default',
}; };
const { custom_virtualenvs } = useContext(ConfigContext); const { custom_virtualenvs } = useContext(ConfigContext);
return (
<>
<FormField
id="org-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
/>
<FormField
id="org-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<FormField
id="org-max_hosts"
name="max_hosts"
type="number"
label={i18n._(t`Max Hosts`)}
tooltip={i18n._(
t`The maximum number of hosts allowed to be managed by this organization.
Value defaults to 0 which means no limit. Refer to the Ansible
documentation for more details.`
)}
validate={minMaxValue(0, Number.MAX_SAFE_INTEGER, i18n)}
me={me || {}}
isDisabled={!me.is_superuser}
/>
{custom_virtualenvs && custom_virtualenvs.length > 1 && (
<FormGroup
fieldId="org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
id="org-custom-virtualenv"
data={[
defaultVenv,
...custom_virtualenvs
.filter(value => value !== defaultVenv.value)
.map(value => ({ value, label: value, key: value })),
]}
{...venvField}
/>
</FormGroup>
)}
<InstanceGroupsLookup
value={instanceGroups}
onChange={setInstanceGroups}
tooltip={i18n._(
t`Select the Instance Groups for this Organization to run on.`
)}
/>
</>
);
}
function OrganizationForm({
organization,
onCancel,
onSubmit,
submitError,
...rest
}) {
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(true); const [hasContentLoading, setHasContentLoading] = useState(true);
const [initialInstanceGroups, setInitialInstanceGroups] = useState([]); const [initialInstanceGroups, setInitialInstanceGroups] = useState([]);
@@ -100,64 +166,11 @@ function OrganizationForm({
> >
{formik => ( {formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}> <Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow> <OrganizationFormFields
<FormField instanceGroups={instanceGroups}
id="org-name" setInstanceGroups={setInstanceGroups}
name="name" {...rest}
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
/> />
<FormField
id="org-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<FormField
id="org-max_hosts"
name="max_hosts"
type="number"
label={i18n._(t`Max Hosts`)}
tooltip={i18n._(
t`The maximum number of hosts allowed to be managed by this organization.
Value defaults to 0 which means no limit. Refer to the Ansible
documentation for more details.`
)}
validate={minMaxValue(0, Number.MAX_SAFE_INTEGER, i18n)}
me={me || {}}
isDisabled={!me.is_superuser}
/>
{custom_virtualenvs && custom_virtualenvs.length > 1 && (
<Field name="custom_virtualenv">
{({ field }) => (
<FormGroup
fieldId="org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
id="org-custom-virtualenv"
data={[
defaultVenv,
...custom_virtualenvs
.filter(value => value !== defaultVenv.value)
.map(value => ({ value, label: value, key: value })),
]}
{...field}
/>
</FormGroup>
)}
</Field>
)}
</FormRow>
<InstanceGroupsLookup
value={instanceGroups}
onChange={setInstanceGroups}
tooltip={i18n._(
t`Select the Instance Groups for this Organization to run on.`
)}
/>
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}

View File

@@ -121,6 +121,11 @@ describe('<OrganizationForm />', () => {
}); });
test('changing inputs and saving triggers expected callback', async () => { test('changing inputs and saving triggers expected callback', async () => {
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups,
},
});
let wrapper; let wrapper;
const onSubmit = jest.fn(); const onSubmit = jest.fn();
await act(async () => { await act(async () => {

View File

@@ -3,9 +3,9 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Formik, Field } from 'formik'; import { Formik, useField } from 'formik';
import { Config } from '@contexts/Config'; import { Config } from '@contexts/Config';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form, FormGroup, Title } from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
@@ -24,17 +24,9 @@ import {
HgSubForm, HgSubForm,
SvnSubForm, SvnSubForm,
InsightsSubForm, InsightsSubForm,
SubFormTitle,
ManualSubForm, ManualSubForm,
} from './ProjectSubForms'; } from './ProjectSubForms';
const ScmTypeFormRow = styled(FormRow)`
background-color: #f5f5f5;
grid-column: 1 / -1;
margin: 0 -24px;
padding: 24px;
`;
const fetchCredentials = async credential => { const fetchCredentials = async credential => {
const [ const [
{ {
@@ -73,14 +65,238 @@ const fetchCredentials = async credential => {
}; };
}; };
function ProjectForm({ project, submitError, ...props }) { function ProjectFormFields({
const { i18n, handleCancel, handleSubmit } = props; project_base_dir,
project_local_paths,
formik,
i18n,
setCredentials,
credentials,
scmTypeOptions,
setScmSubFormState,
scmSubFormState,
setOrganization,
organization,
}) {
const scmFormFields = {
scm_url: '',
scm_branch: '',
scm_refspec: '',
credential: '',
scm_clean: false,
scm_delete_on_update: false,
scm_update_on_launch: false,
allow_override: false,
scm_update_cache_timeout: 0,
};
const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({
name: 'scm_type',
validate: required(i18n._(t`Set a value for this field`), i18n),
});
const [venvField] = useField('custom_virtualenv');
const orgFieldArr = useField({
name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const organizationMeta = orgFieldArr[1];
const organizationHelpers = orgFieldArr[2];
/* Save current scm subform field values to state */
const saveSubFormState = form => {
const currentScmFormFields = { ...scmFormFields };
Object.keys(currentScmFormFields).forEach(label => {
currentScmFormFields[label] = form.values[label];
});
setScmSubFormState(currentScmFormFields);
};
/**
* If scm type is !== the initial scm type value,
* reset scm subform field values to defaults.
* If scm type is === the initial scm type value,
* reset scm subform field values to scmSubFormState.
*/
const resetScmTypeFields = (value, form) => {
if (form.values.scm_type === form.initialValues.scm_type) {
saveSubFormState(formik);
}
Object.keys(scmFormFields).forEach(label => {
if (value === form.initialValues.scm_type) {
form.setFieldValue(label, scmSubFormState[label]);
} else {
form.setFieldValue(label, scmFormFields[label]);
}
form.setFieldTouched(label, false);
});
};
const handleCredentialSelection = (type, value) => {
setCredentials({
...credentials,
[type]: {
...credentials[type],
value,
},
});
};
return (
<>
<FormField
id="project-name"
label={i18n._(t`Name`)}
name="name"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="project-description"
label={i18n._(t`Description`)}
name="description"
type="text"
/>
<OrganizationLookup
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value.id);
setOrganization(value);
}}
value={organization}
required
/>
<FormGroup
fieldId="project-scm-type"
helperTextInvalid={scmTypeMeta.error}
isRequired
isValid={!scmTypeMeta.touched || !scmTypeMeta.error}
label={i18n._(t`SCM Type`)}
>
<AnsibleSelect
{...scmTypeField}
id="scm_type"
data={[
{
value: '',
key: '',
label: i18n._(t`Choose an SCM Type`),
isDisabled: true,
},
...scmTypeOptions.map(([value, label]) => {
if (label === 'Manual') {
value = 'manual';
}
return {
label,
value,
key: value,
};
}),
]}
onChange={(event, value) => {
scmTypeHelpers.setValue(value);
resetScmTypeFields(value, formik);
}}
/>
</FormGroup>
{formik.values.scm_type !== '' && (
<ScmTypeFormRow>
<SubFormTitle size="md">
{i18n._(t`Type Details`)}
</SubFormTitle>
{
{
manual: (
<ManualSubForm
localPath={formik.initialValues.local_path}
project_base_dir={project_base_dir}
project_local_paths={project_local_paths}
/>
),
git: (
<GitSubForm
credential={credentials.scm}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/>
),
hg: (
<HgSubForm
credential={credentials.scm}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/>
),
svn: (
<SvnSubForm
credential={credentials.scm}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/>
),
insights: (
<InsightsSubForm
credential={credentials.insights}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/>
),
}[formik.values.scm_type]
}
</ScmTypeFormRow>
)}
<Config>
{({ custom_virtualenvs }) =>
custom_virtualenvs &&
custom_virtualenvs.length > 1 && (
<Field name="custom_virtualenv">
{({ field }) => (
<FormGroup
fieldId="project-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<FieldTooltip
content={i18n._(t`Select the playbook to be executed by
this job.`)}
/>
<AnsibleSelect
id="project-custom-virtualenv"
data={[
{
label: i18n._(t`Use Default Ansible Environment`),
value: '/venv/ansible/',
key: 'default',
},
...custom_virtualenvs
.filter(datum => datum !== '/venv/ansible/')
.map(datum => ({
label: datum,
value: datum,
key: datum,
})),
]}
{...venvField}
/>
</FormGroup>
)
}
</Config>
</>
);
}
function ProjectForm({ i18n, project, submitError, ...props }) {
const { handleCancel, handleSubmit } = props;
const { summary_fields = {} } = project; const { summary_fields = {} } = project;
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [organization, setOrganization] = useState( const [organization, setOrganization] = useState(null);
summary_fields.organization || null
);
const [scmSubFormState, setScmSubFormState] = useState(null); const [scmSubFormState, setScmSubFormState] = useState(null);
const [scmTypeOptions, setScmTypeOptions] = useState(null); const [scmTypeOptions, setScmTypeOptions] = useState(null);
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
@@ -114,60 +330,6 @@ function ProjectForm({ project, submitError, ...props }) {
fetchData(); fetchData();
}, [summary_fields.credential]); }, [summary_fields.credential]);
const scmFormFields = {
scm_url: '',
scm_branch: '',
scm_refspec: '',
credential: '',
scm_clean: false,
scm_delete_on_update: false,
scm_update_on_launch: false,
allow_override: false,
scm_update_cache_timeout: 0,
};
/* Save current scm subform field values to state */
const saveSubFormState = form => {
const currentScmFormFields = { ...scmFormFields };
Object.keys(currentScmFormFields).forEach(label => {
currentScmFormFields[label] = form.values[label];
});
setScmSubFormState(currentScmFormFields);
};
/**
* If scm type is !== the initial scm type value,
* reset scm subform field values to defaults.
* If scm type is === the initial scm type value,
* reset scm subform field values to scmSubFormState.
*/
const resetScmTypeFields = (value, form) => {
if (form.values.scm_type === form.initialValues.scm_type) {
saveSubFormState(form);
}
Object.keys(scmFormFields).forEach(label => {
if (value === form.initialValues.scm_type) {
form.setFieldValue(label, scmSubFormState[label]);
} else {
form.setFieldValue(label, scmFormFields[label]);
}
form.setFieldTouched(label, false);
});
};
const handleCredentialSelection = (type, value) => {
setCredentials({
...credentials,
[type]: {
...credentials[type],
value,
},
});
};
if (isLoading) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
} }
@@ -211,183 +373,19 @@ function ProjectForm({ project, submitError, ...props }) {
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
css="padding: 0 24px" css="padding: 0 24px"
> >
<FormRow> <ProjectFormFields
<FormField project_base_dir={project_base_dir}
id="project-name" project_local_paths={project_local_paths}
label={i18n._(t`Name`)} formik={formik}
name="name" i18n={i18n}
type="text" setCredentials={setCredentials}
validate={required(null, i18n)} credentials={credentials}
isRequired scmTypeOptions={scmTypeOptions}
setScmSubFormState={setScmSubFormState}
scmSubFormState={scmSubFormState}
setOrganization={setOrganization}
organization={organization}
/> />
<FormField
id="project-description"
label={i18n._(t`Description`)}
name="description"
type="text"
/>
<Field
name="organization"
validate={required(
i18n._(t`Select a value for this field`),
i18n
)}
>
{({ form }) => (
<OrganizationLookup
helperTextInvalid={form.errors.organization}
isValid={
!form.touched.organization || !form.errors.organization
}
onBlur={() => form.setFieldTouched('organization')}
onChange={value => {
form.setFieldValue('organization', value.id);
setOrganization(value);
}}
value={organization}
required
/>
)}
</Field>
<Field
name="scm_type"
validate={required(
i18n._(t`Select a value for this field`),
i18n
)}
>
{({ field, form }) => (
<FormGroup
fieldId="project-scm-type"
helperTextInvalid={form.errors.scm_type}
isRequired
isValid={!form.touched.scm_type || !form.errors.scm_type}
label={i18n._(t`SCM Type`)}
>
<AnsibleSelect
{...field}
id="scm_type"
data={[
{
value: '',
key: '',
label: i18n._(t`Choose an SCM Type`),
isDisabled: true,
},
...scmTypeOptions.map(([value, label]) => {
if (label === 'Manual') {
value = 'manual';
}
return {
label,
value,
key: value,
};
}),
]}
onChange={(event, value) => {
form.setFieldValue('scm_type', value);
resetScmTypeFields(value, form);
}}
/>
</FormGroup>
)}
</Field>
{formik.values.scm_type !== '' && (
<ScmTypeFormRow>
<SubFormTitle size="md">
{i18n._(t`Type Details`)}
</SubFormTitle>
{
{
manual: (
<ManualSubForm
localPath={formik.initialValues.local_path}
project_base_dir={project_base_dir}
project_local_paths={project_local_paths}
/>
),
git: (
<GitSubForm
credential={credentials.scm}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={
formik.values.scm_update_on_launch
}
/>
),
hg: (
<HgSubForm
credential={credentials.scm}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={
formik.values.scm_update_on_launch
}
/>
),
svn: (
<SvnSubForm
credential={credentials.scm}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={
formik.values.scm_update_on_launch
}
/>
),
insights: (
<InsightsSubForm
credential={credentials.insights}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={
formik.values.scm_update_on_launch
}
/>
),
}[formik.values.scm_type]
}
</ScmTypeFormRow>
)}
<Config>
{({ custom_virtualenvs }) =>
custom_virtualenvs &&
custom_virtualenvs.length > 1 && (
<Field name="custom_virtualenv">
{({ field }) => (
<FormGroup
fieldId="project-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<FieldTooltip
content={i18n._(t`Select the playbook to be executed by
this job.`)}
/>
<AnsibleSelect
id="project-custom-virtualenv"
data={[
{
label: i18n._(
t`Use Default Ansible Environment`
),
value: '/venv/ansible/',
key: 'default',
},
...custom_virtualenvs
.filter(datum => datum !== '/venv/ansible/')
.map(datum => ({
label: datum,
value: datum,
key: datum,
})),
]}
{...field}
/>
</FormGroup>
)}
</Field>
)
}
</Config>
</FormRow>
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Field } from 'formik'; import { useField } from 'formik';
import CredentialLookup from '@components/Lookup/CredentialLookup'; import CredentialLookup from '@components/Lookup/CredentialLookup';
import { required } from '@util/validators'; import { required } from '@util/validators';
import { ScmTypeOptions } from './SharedFields'; import { ScmTypeOptions } from './SharedFields';
@@ -11,30 +11,32 @@ const InsightsSubForm = ({
credential, credential,
onCredentialSelection, onCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
}) => ( }) => {
<> const credFieldArr = useField({
<Field name: 'credential',
name="credential" validate: required(i18n._(t`Select a value for this field`), i18n),
validate={required(i18n._(t`Select a value for this field`), i18n)} });
> const credMeta = credFieldArr[1];
{({ form }) => ( const credHelpers = credFieldArr[2];
<CredentialLookup
credentialTypeId={credential.typeId} return (
label={i18n._(t`Insights Credential`)} <>
helperTextInvalid={form.errors.credential} <CredentialLookup
isValid={!form.touched.credential || !form.errors.credential} credentialTypeId={credential.typeId}
onBlur={() => form.setFieldTouched('credential')} label={i18n._(t`Insights Credential`)}
onChange={value => { helperTextInvalid={credMeta.error}
onCredentialSelection('insights', value); isValid={!credMeta.touched || !credMeta.error}
form.setFieldValue('credential', value.id); onBlur={() => credHelpers.setTouched()}
}} onChange={value => {
value={credential.value} onCredentialSelection('insights', value);
required credHelpers.setValue(value.id);
/> }}
)} value={credential.value}
</Field> required
<ScmTypeOptions hideAllowOverride scmUpdateOnLaunch={scmUpdateOnLaunch} /> />
</> <ScmTypeOptions hideAllowOverride scmUpdateOnLaunch={scmUpdateOnLaunch} />
); </>
);
};
export default withI18n()(InsightsSubForm); export default withI18n()(InsightsSubForm);

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Field } from 'formik'; import { useField } from 'formik';
import { required } from '@util/validators'; import { required } from '@util/validators';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import FormField, { FieldTooltip } from '@components/FormField'; import FormField, { FieldTooltip } from '@components/FormField';
@@ -34,6 +34,10 @@ const ManualSubForm = ({
label: path, label: path,
})), })),
]; ];
const [pathField, pathMeta, pathHelpers] = useField({
name: 'local_path',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
return ( return (
<> <>
@@ -72,35 +76,27 @@ const ManualSubForm = ({
</span> </span>
} }
/> />
{options.length !== 1 && ( <FormGroup
<Field fieldId="project-local-path"
name="local_path" helperTextInvalid={pathMeta.error}
validate={required(i18n._(t`Select a value for this field`), i18n)} isRequired
> isValid={!pathMeta.touched || !pathMeta.error}
{({ field, form }) => ( label={i18n._(t`Playbook Directory`)}
<FormGroup >
fieldId="project-local-path" <FieldTooltip
helperTextInvalid={form.errors.local_path} content={i18n._(t`Select from the list of directories found in
isRequired the Project Base Path. Together the base path and the playbook
isValid={!form.touched.local_path || !form.errors.local_path} directory provide the full path used to locate playbooks.`)}
label={i18n._(t`Playbook Directory`)} />
> <AnsibleSelect
<FieldTooltip {...pathField}
content={i18n._(t`Select from the list of directories found in id="local_path"
the Project Base Path. Together the base path and the playbook data={options}
directory provide the full path used to locate playbooks.`)} onChange={(event, value) => {
/> pathHelpers.setValue(value);
<AnsibleSelect }}
{...field} />
id="local_path" </FormGroup>
data={options}
onChange={(event, value) => {
form.setFieldValue('local_path', value);
}}
/>
</FormGroup>
)}
</Field>
)} )}
</> </>
); );

View File

@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Field } from 'formik'; import { useField } from 'formik';
import CredentialLookup from '@components/Lookup/CredentialLookup'; import CredentialLookup from '@components/Lookup/CredentialLookup';
import FormField, { CheckboxField } from '@components/FormField'; import FormField, { CheckboxField } from '@components/FormField';
import { required } from '@util/validators'; import { required } from '@util/validators';
import FormRow from '@components/FormRow';
import { FormGroup, Title } from '@patternfly/react-core'; import { FormGroup, Title } from '@patternfly/react-core';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -41,21 +40,21 @@ export const BranchFormField = withI18n()(({ i18n, label }) => (
)); ));
export const ScmCredentialFormField = withI18n()( export const ScmCredentialFormField = withI18n()(
({ i18n, credential, onCredentialSelection }) => ( ({ i18n, credential, onCredentialSelection }) => {
<Field name="credential"> const credHelpers = useField('credential')[2];
{({ form }) => (
return (
<CredentialLookup <CredentialLookup
credentialTypeId={credential.typeId} credentialTypeId={credential.typeId}
label={i18n._(t`SCM Credential`)} label={i18n._(t`SCM Credential`)}
value={credential.value} value={credential.value}
onChange={value => { onChange={value => {
onCredentialSelection('scm', value); onCredentialSelection('scm', value);
form.setFieldValue('credential', value ? value.id : ''); credHelpers.setValue(value ? value.id : '');
}} }}
/> />
)} );
</Field> }
)
); );
export const ScmTypeOptions = withI18n()( export const ScmTypeOptions = withI18n()(

View File

@@ -2,19 +2,58 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Formik, Field } from 'formik'; import { Formik, useField } from 'formik';
import { Form } from '@patternfly/react-core'; import { Form } from '@patternfly/react-core';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormField, { FormSubmitError } from '@components/FormField'; import FormField, { FormSubmitError } from '@components/FormField';
import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required } from '@util/validators'; import { required } from '@util/validators';
function TeamForm(props) { function TeamFormFields(props) {
const { team, handleCancel, handleSubmit, submitError, i18n } = props; const { team, i18n } = props;
const [organization, setOrganization] = useState( const [organization, setOrganization] = useState(
team.summary_fields ? team.summary_fields.organization : null team.summary_fields ? team.summary_fields.organization : null
); );
const orgFieldArr = useField({
name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const orgMeta = orgFieldArr[1];
const orgHelpers = orgFieldArr[2];
return (
<>
<FormField
id="team-name"
label={i18n._(t`Name`)}
name="name"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="team-description"
label={i18n._(t`Description`)}
name="description"
type="text"
/>
<OrganizationLookup
helperTextInvalid={orgMeta.error}
isValid={!orgMeta.touched || !orgMeta.error}
onBlur={() => orgHelpers.setTouched('organization')}
onChange={value => {
orgHelpers.setValue(value.id);
setOrganization(value);
}}
value={organization}
required
/>
</>
);
}
function TeamForm(props) {
const { team, handleCancel, handleSubmit, submitError, ...rest } = props;
return ( return (
<Formik <Formik
@@ -31,45 +70,7 @@ function TeamForm(props) {
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
css="padding: 0 24px" css="padding: 0 24px"
> >
<FormRow> <TeamFormFields team={team} {...rest} />
<FormField
id="team-name"
label={i18n._(t`Name`)}
name="name"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="team-description"
label={i18n._(t`Description`)}
name="description"
type="text"
/>
<Field
name="organization"
validate={required(
i18n._(t`Select a value for this field`),
i18n
)}
>
{({ form }) => (
<OrganizationLookup
helperTextInvalid={form.errors.organization}
isValid={
!form.touched.organization || !form.errors.organization
}
onBlur={() => form.setFieldTouched('organization')}
onChange={value => {
form.setFieldValue('organization', value.id);
setOrganization(value);
}}
value={organization}
required
/>
)}
</Field>
</FormRow>
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Formik, Field } from 'formik'; import { Formik, useField } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form, FormGroup } from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
@@ -14,7 +14,7 @@ import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required, requiredEmail } from '@util/validators'; import { required, requiredEmail } from '@util/validators';
function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) { function UserFormFields({ user, i18n }) {
const [organization, setOrganization] = useState(null); const [organization, setOrganization] = useState(null);
const userTypeOptions = [ const userTypeOptions = [
@@ -38,6 +38,98 @@ function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) {
}, },
]; ];
const organizationFieldArr = useField({
name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const organizationMeta = organizationFieldArr[1];
const organizationHelpers = organizationFieldArr[2];
const [userTypeField, userTypeMeta] = useField('user_type');
return (
<>
<FormField
id="user-username"
label={i18n._(t`Username`)}
name="username"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="user-email"
label={i18n._(t`Email`)}
name="email"
validate={requiredEmail(i18n)}
isRequired
/>
<PasswordField
id="user-password"
label={i18n._(t`Password`)}
name="password"
validate={
!user.id
? required(i18n._(t`This field must not be blank`), i18n)
: () => undefined
}
isRequired={!user.id}
/>
<PasswordField
id="user-confirm-password"
label={i18n._(t`Confirm Password`)}
name="confirm_password"
validate={
!user.id
? required(i18n._(t`This field must not be blank`), i18n)
: () => undefined
}
isRequired={!user.id}
/>
<FormField
id="user-first-name"
label={i18n._(t`First Name`)}
name="first_name"
type="text"
/>
<FormField
id="user-last-name"
label={i18n._(t`Last Name`)}
name="last_name"
type="text"
/>
{!user.id && (
<OrganizationLookup
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value.id);
setOrganization(value);
}}
value={organization}
required
/>
)}
<FormGroup
fieldId="user-type"
helperTextInvalid={userTypeMeta.error}
isRequired
isValid={!userTypeMeta.touched || !userTypeMeta.error}
label={i18n._(t`User Type`)}
>
<AnsibleSelect
isValid={!userTypeMeta.touched || !userTypeMeta.error}
id="user-type"
data={userTypeOptions}
{...userTypeField}
/>
</FormGroup>
</>
);
}
function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) {
const handleValidateAndSubmit = (values, { setErrors }) => { const handleValidateAndSubmit = (values, { setErrors }) => {
if (values.password !== values.confirm_password) { if (values.password !== values.confirm_password) {
setErrors({ setErrors({
@@ -81,106 +173,7 @@ function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) {
> >
{formik => ( {formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}> <Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow> <UserFormFields user={user} i18n={i18n} />
<FormField
id="user-username"
label={i18n._(t`Username`)}
name="username"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="user-email"
label={i18n._(t`Email`)}
name="email"
validate={requiredEmail(i18n)}
isRequired
/>
<PasswordField
id="user-password"
label={i18n._(t`Password`)}
name="password"
validate={
!user.id
? required(i18n._(t`This field must not be blank`), i18n)
: () => undefined
}
isRequired={!user.id}
/>
<PasswordField
id="user-confirm-password"
label={i18n._(t`Confirm Password`)}
name="confirm_password"
validate={
!user.id
? required(i18n._(t`This field must not be blank`), i18n)
: () => undefined
}
isRequired={!user.id}
/>
</FormRow>
<FormRow>
<FormField
id="user-first-name"
label={i18n._(t`First Name`)}
name="first_name"
type="text"
/>
<FormField
id="user-last-name"
label={i18n._(t`Last Name`)}
name="last_name"
type="text"
/>
{!user.id && (
<Field
name="organization"
validate={required(
i18n._(t`Select a value for this field`),
i18n
)}
>
{({ form }) => (
<OrganizationLookup
helperTextInvalid={form.errors.organization}
isValid={
!form.touched.organization || !form.errors.organization
}
onBlur={() => form.setFieldTouched('organization')}
onChange={value => {
form.setFieldValue('organization', value.id);
setOrganization(value);
}}
value={organization}
required
/>
)}
</Field>
)}
<Field name="user_type">
{({ form, field }) => {
const isValid =
!form.touched.user_type || !form.errors.user_type;
return (
<FormGroup
fieldId="user-type"
helperTextInvalid={form.errors.user_type}
isRequired
isValid={isValid}
label={i18n._(t`User Type`)}
>
<AnsibleSelect
isValid={isValid}
id="user-type"
data={userTypeOptions}
{...field}
/>
</FormGroup>
);
}}
</Field>
</FormRow>
<FormSubmitError error={submitError} /> <FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}