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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Formik, Field } from 'formik';
import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { func, number, shape } from 'prop-types';
@ -8,44 +8,25 @@ import { VariablesField } from '@components/CodeMirrorInput';
import { Form } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormRow from '@components/FormRow';
import { required } from '@util/validators';
import InstanceGroupsLookup from '@components/Lookup/InstanceGroupsLookup';
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import CredentialLookup from '@components/Lookup/CredentialLookup';
function InventoryForm({
inventory = {},
i18n,
onCancel,
onSubmit,
instanceGroups,
credentialTypeId,
submitError,
}) {
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,
};
function InventoryFormFields({ i18n, credentialTypeId }) {
const [organizationField, organizationMeta, organizationHelpers] = useField({
name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const instanceGroupsFieldArr = useField('instanceGroups');
const instanceGroupsField = instanceGroupsFieldArr[0];
const instanceGroupsHelpers = instanceGroupsFieldArr[2];
const insightsCredentialFieldArr = useField('insights_credential');
const insightsCredentialField = insightsCredentialFieldArr[0];
const insightsCredentialHelpers = insightsCredentialFieldArr[2];
return (
<Formik
initialValues={initialValues}
onSubmit={values => {
onSubmit(values);
}}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<>
<FormField
id="inventory-name"
label={i18n._(t`Name`)}
@ -60,66 +41,30 @@ function InventoryForm({
name="description"
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
helperTextInvalid={form.errors.organization}
isValid={
!form.touched.organization || !form.errors.organization
}
onBlur={() => form.setFieldTouched('organization')}
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
form.setFieldValue('organization', value);
organizationHelpers.setValue(value);
}}
value={field.value}
touched={form.touched.organization}
error={form.errors.organization}
value={organizationField.value}
touched={organizationMeta.touched}
error={organizationMeta.error}
required
/>
)}
</Field>
<Field
id="inventory-insights_credential"
label={i18n._(t`Insights Credential`)}
name="insights_credential"
>
{({ field, form }) => (
<CredentialLookup
label={i18n._(t`Insights Credential`)}
credentialTypeId={credentialTypeId}
onChange={value =>
form.setFieldValue('insights_credential', value)
}
value={field.value}
onChange={value => insightsCredentialHelpers.setValue(value)}
value={insightsCredentialField.value}
/>
)}
</Field>
</FormRow>
<FormRow>
<Field
id="inventory-instanceGroups"
label={i18n._(t`Instance Groups`)}
name="instanceGroups"
>
{({ field, form }) => (
<InstanceGroupsLookup
value={field.value}
value={instanceGroupsField.value}
onChange={value => {
form.setFieldValue('instanceGroups', value);
instanceGroupsHelpers.setValue(value);
}}
/>
)}
</Field>
</FormRow>
<FormRow>
<VariablesField
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`
@ -128,8 +73,42 @@ function InventoryForm({
name="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} />
<FormActionGroup
onCancel={onCancel}

View File

@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik';
import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Form, FormGroup } from '@patternfly/react-core';
@ -18,20 +18,86 @@ import { InstanceGroupsLookup } from '@components/Lookup/';
import { getAddedAndRemoved } from '@util/lists';
import { required, minMaxValue } from '@util/validators';
function OrganizationForm({
organization,
function OrganizationFormFields({
i18n,
me,
onCancel,
onSubmit,
submitError,
instanceGroups,
setInstanceGroups,
}) {
const [venvField] = useField('custom_virtualenv');
const defaultVenv = {
label: i18n._(t`Use Default Ansible Environment`),
value: '/venv/ansible/',
key: 'default',
};
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 [hasContentLoading, setHasContentLoading] = useState(true);
const [initialInstanceGroups, setInitialInstanceGroups] = useState([]);
@ -100,64 +166,11 @@ function OrganizationForm({
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<FormField
id="org-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
<OrganizationFormFields
instanceGroups={instanceGroups}
setInstanceGroups={setInstanceGroups}
{...rest}
/>
<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} />
<FormActionGroup
onCancel={handleCancel}

View File

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

View File

@ -3,9 +3,9 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, Field } from 'formik';
import { Formik, useField } from 'formik';
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 ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
@ -24,17 +24,9 @@ import {
HgSubForm,
SvnSubForm,
InsightsSubForm,
SubFormTitle,
ManualSubForm,
} from './ProjectSubForms';
const ScmTypeFormRow = styled(FormRow)`
background-color: #f5f5f5;
grid-column: 1 / -1;
margin: 0 -24px;
padding: 24px;
`;
const fetchCredentials = async credential => {
const [
{
@ -73,14 +65,238 @@ const fetchCredentials = async credential => {
};
};
function ProjectForm({ project, submitError, ...props }) {
const { i18n, handleCancel, handleSubmit } = props;
function ProjectFormFields({
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 [contentError, setContentError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [organization, setOrganization] = useState(
summary_fields.organization || null
);
const [organization, setOrganization] = useState(null);
const [scmSubFormState, setScmSubFormState] = useState(null);
const [scmTypeOptions, setScmTypeOptions] = useState(null);
const [credentials, setCredentials] = useState({
@ -114,60 +330,6 @@ function ProjectForm({ project, submitError, ...props }) {
fetchData();
}, [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) {
return <ContentLoading />;
}
@ -211,183 +373,19 @@ function ProjectForm({ project, submitError, ...props }) {
onSubmit={formik.handleSubmit}
css="padding: 0 24px"
>
<FormRow>
<FormField
id="project-name"
label={i18n._(t`Name`)}
name="name"
type="text"
validate={required(null, i18n)}
isRequired
<ProjectFormFields
project_base_dir={project_base_dir}
project_local_paths={project_local_paths}
formik={formik}
i18n={i18n}
setCredentials={setCredentials}
credentials={credentials}
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} />
<FormActionGroup
onCancel={handleCancel}

View File

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

View File

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

View File

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

View File

@ -2,19 +2,58 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, Field } from 'formik';
import { Formik, useField } from 'formik';
import { Form } from '@patternfly/react-core';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormField, { FormSubmitError } from '@components/FormField';
import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required } from '@util/validators';
function TeamForm(props) {
const { team, handleCancel, handleSubmit, submitError, i18n } = props;
function TeamFormFields(props) {
const { team, i18n } = props;
const [organization, setOrganization] = useState(
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 (
<Formik
@ -31,45 +70,7 @@ function TeamForm(props) {
onSubmit={formik.handleSubmit}
css="padding: 0 24px"
>
<FormRow>
<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>
<TeamFormFields team={team} {...rest} />
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, Field } from 'formik';
import { Formik, useField } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
@ -14,7 +14,7 @@ import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required, requiredEmail } from '@util/validators';
function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) {
function UserFormFields({ user, i18n }) {
const [organization, setOrganization] = useState(null);
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 }) => {
if (values.password !== values.confirm_password) {
setErrors({
@ -81,106 +173,7 @@ function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) {
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<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>
<UserFormFields user={user} i18n={i18n} />
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}