Merge pull request #6385 from AlexSCorey/6317-ConvertJTFormstoFormikHooks

Uses formik hooks for JT Form

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-04-02 15:52:35 +00:00
committed by GitHub
8 changed files with 164 additions and 249 deletions

View File

@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
import { CardBody } from '@components/Card'; import { CardBody } from '@components/Card';
import JobTemplateForm from '../shared/JobTemplateForm'; import JobTemplateForm from '../shared/JobTemplateForm';
import { JobTemplatesAPI, OrganizationsAPI } from '@api'; import { JobTemplatesAPI } from '@api';
function JobTemplateAdd() { function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null); const [formSubmitError, setFormSubmitError] = useState(null);
@@ -12,7 +12,6 @@ function JobTemplateAdd() {
async function handleSubmit(values) { async function handleSubmit(values) {
const { const {
labels, labels,
organizationId,
instanceGroups, instanceGroups,
initialInstanceGroups, initialInstanceGroups,
credentials, credentials,
@@ -20,12 +19,13 @@ function JobTemplateAdd() {
} = values; } = values;
setFormSubmitError(null); setFormSubmitError(null);
remainingValues.project = remainingValues.project.id;
try { try {
const { const {
data: { id, type }, data: { id, type },
} = await JobTemplatesAPI.create(remainingValues); } = await JobTemplatesAPI.create(remainingValues);
await Promise.all([ await Promise.all([
submitLabels(id, labels, organizationId), submitLabels(id, labels, values.project.summary_fields.organization.id),
submitInstanceGroups(id, instanceGroups), submitInstanceGroups(id, instanceGroups),
submitCredentials(id, credentials), submitCredentials(id, credentials),
]); ]);
@@ -35,16 +35,7 @@ function JobTemplateAdd() {
} }
} }
async function submitLabels(templateId, labels = [], formOrg) { async function submitLabels(templateId, labels = [], orgId) {
let orgId = formOrg;
if (!orgId && labels.length > 0) {
const {
data: { results },
} = await OrganizationsAPI.read();
orgId = results[0].id;
}
const associationPromises = labels.map(label => const associationPromises = labels.map(label =>
JobTemplatesAPI.associateLabel(templateId, label, orgId) JobTemplatesAPI.associateLabel(templateId, label, orgId)
); );

View File

@@ -33,7 +33,7 @@ const jobTemplateData = {
limit: '', limit: '',
name: '', name: '',
playbook: '', playbook: '',
project: 1, project: { id: 1, summary_fields: { organization: { id: 1 } } },
scm_branch: '', scm_branch: '',
skip_tags: '', skip_tags: '',
timeout: 0, timeout: 0,
@@ -123,6 +123,7 @@ describe('<JobTemplateAdd />', () => {
wrapper.find('ProjectLookup').invoke('onChange')({ wrapper.find('ProjectLookup').invoke('onChange')({
id: 2, id: 2,
name: 'project', name: 'project',
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
}); });
wrapper.update(); wrapper.update();
wrapper wrapper
@@ -161,6 +162,7 @@ describe('<JobTemplateAdd />', () => {
id: 1, id: 1,
type: 'job_template', type: 'job_template',
...jobTemplateData, ...jobTemplateData,
project: jobTemplateData.project.id,
}, },
}); });
let wrapper; let wrapper;
@@ -181,6 +183,7 @@ describe('<JobTemplateAdd />', () => {
wrapper.find('ProjectLookup').invoke('onChange')({ wrapper.find('ProjectLookup').invoke('onChange')({
id: 2, id: 2,
name: 'project', name: 'project',
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
}); });
wrapper.update(); wrapper.update();
wrapper wrapper

View File

@@ -4,7 +4,7 @@ import { withRouter, Redirect } from 'react-router-dom';
import { CardBody } from '@components/Card'; import { CardBody } from '@components/Card';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import { JobTemplatesAPI, OrganizationsAPI, ProjectsAPI } from '@api'; import { JobTemplatesAPI, ProjectsAPI } from '@api';
import { JobTemplate } from '@types'; import { JobTemplate } from '@types';
import { getAddedAndRemoved } from '@util/lists'; import { getAddedAndRemoved } from '@util/lists';
import JobTemplateForm from '../shared/JobTemplateForm'; import JobTemplateForm from '../shared/JobTemplateForm';
@@ -97,7 +97,6 @@ class JobTemplateEdit extends Component {
const { template, history } = this.props; const { template, history } = this.props;
const { const {
labels, labels,
organizationId,
instanceGroups, instanceGroups,
initialInstanceGroups, initialInstanceGroups,
credentials, credentials,
@@ -105,10 +104,14 @@ class JobTemplateEdit extends Component {
} = values; } = values;
this.setState({ formSubmitError: null }); this.setState({ formSubmitError: null });
remainingValues.project = values.project.id;
try { try {
await JobTemplatesAPI.update(template.id, remainingValues); await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([ await Promise.all([
this.submitLabels(labels, organizationId), this.submitLabels(
labels,
values.project.summary_fields.organization.id
),
this.submitInstanceGroups(instanceGroups, initialInstanceGroups), this.submitInstanceGroups(instanceGroups, initialInstanceGroups),
this.submitCredentials(credentials), this.submitCredentials(credentials),
]); ]);
@@ -118,22 +121,14 @@ class JobTemplateEdit extends Component {
} }
} }
async submitLabels(labels = [], formOrgId) { async submitLabels(labels = [], orgId) {
const { template } = this.props; const { template } = this.props;
let orgId = formOrgId;
const { added, removed } = getAddedAndRemoved( const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results, template.summary_fields.labels.results,
labels labels
); );
if (!orgId && added.length > 0) {
const {
data: { results },
} = await OrganizationsAPI.read();
orgId = results[0].id;
}
const disassociationPromises = removed.map(label => const disassociationPromises = removed.map(label =>
JobTemplatesAPI.disassociateLabel(template.id, label) JobTemplatesAPI.disassociateLabel(template.id, label)
); );

View File

@@ -35,7 +35,7 @@ const mockJobTemplate = {
limit: '', limit: '',
name: 'Foo', name: 'Foo',
playbook: 'Baz', playbook: 'Baz',
project: 3, project: { id: 3, summary_fields: { organization: { id: 1 } } },
scm_branch: '', scm_branch: '',
skip_tags: '', skip_tags: '',
summary_fields: { summary_fields: {
@@ -239,6 +239,7 @@ describe('<JobTemplateEdit />', () => {
const expected = { const expected = {
...mockJobTemplate, ...mockJobTemplate,
project: mockJobTemplate.project.id,
...updatedTemplateData, ...updatedTemplateData,
}; };
delete expected.summary_fields; delete expected.summary_fields;

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withFormik, Field } from 'formik'; import { withFormik, useField, useFormikContext } from 'formik';
import { import {
Form, Form,
FormGroup, FormGroup,
@@ -47,11 +47,9 @@ function JobTemplateForm({
validateField, validateField,
handleCancel, handleCancel,
handleSubmit, handleSubmit,
handleBlur,
setFieldValue, setFieldValue,
submitError, submitError,
i18n, i18n,
touched,
}) { }) {
const [contentError, setContentError] = useState(false); const [contentError, setContentError] = useState(false);
const [project, setProject] = useState(null); const [project, setProject] = useState(null);
@@ -62,6 +60,35 @@ function JobTemplateForm({
Boolean(template?.host_config_key) Boolean(template?.host_config_key)
); );
const { values: formikValues } = useFormikContext();
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
name: 'job_type',
validate: required(null, i18n),
});
const [, inventoryMeta, inventoryHelpers] = useField('inventory');
const [projectField, projectMeta, projectHelpers] = useField({
name: 'project',
validate: () => handleProjectValidation(),
});
const [scmField, , scmHelpers] = useField('scm_branch');
const [playbookField, playbookMeta, playbookHelpers] = useField({
name: 'playbook',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const [credentialField, , credentialHelpers] = useField('credentials');
const [labelsField, , labelsHelpers] = useField('labels');
const [limitField, limitMeta] = useField('limit');
const [verbosityField] = useField('verbosity');
const [diffModeField, , diffModeHelpers] = useField('diff_mode');
const [instanceGroupsField, , instanceGroupsHelpers] = useField(
'instanceGroups'
);
const [jobTagsField, , jobTagsHelpers] = useField('job_tags');
const [skipTagsField, , skipTagsHelpers] = useField('skip_tags');
const { const {
request: fetchProject, request: fetchProject,
error: projectContentError, error: projectContentError,
@@ -100,7 +127,7 @@ function JobTemplateForm({
}, [loadRelatedInstanceGroups]); }, [loadRelatedInstanceGroups]);
const handleProjectValidation = () => { const handleProjectValidation = () => {
if (!project && touched.project) { if (!project && projectMeta.touched) {
return i18n._(t`Select a value for this field`); return i18n._(t`Select a value for this field`);
} }
if (project && project.status === 'never updated') { if (project && project.status === 'never updated') {
@@ -112,11 +139,11 @@ function JobTemplateForm({
const handleProjectUpdate = useCallback( const handleProjectUpdate = useCallback(
newProject => { newProject => {
setProject(newProject); setProject(newProject);
setFieldValue('project', newProject.id); projectHelpers.setValue(newProject);
setFieldValue('playbook', 0); playbookHelpers.setValue(0);
setFieldValue('scm_branch', ''); scmHelpers.setValue('');
}, },
[setFieldValue, setProject] [setProject, projectHelpers, playbookHelpers, scmHelpers]
); );
const jobTypeOptions = [ const jobTypeOptions = [
@@ -184,26 +211,15 @@ function JobTemplateForm({
test environment setup, and report problems without test environment setup, and report problems without
executing the playbook.`)} executing the playbook.`)}
> >
<Field <AnsibleSelect
name="job_type" {...jobTypeField}
validate={required(null, i18n)} isValid={!jobTypeMeta.touched || !jobTypeMeta.error}
onBlur={handleBlur} id="template-job-type"
> data={jobTypeOptions}
{({ form, field }) => { onChange={(event, value) => {
const isValid = !form.touched.job_type || !form.errors.job_type; jobTypeHelpers.setValue(value);
return (
<AnsibleSelect
isValid={isValid}
id="template-job-type"
data={jobTypeOptions}
{...field}
onChange={(event, value) => {
form.setFieldValue('job_type', value);
}}
/>
);
}} }}
</Field> />
</FieldWithPrompt> </FieldWithPrompt>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-inventory" fieldId="template-inventory"
@@ -214,108 +230,71 @@ function JobTemplateForm({
tooltip={i18n._(t`Select the inventory containing the hosts tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)} you want this job to manage.`)}
> >
<Field name="inventory"> <InventoryLookup
{({ form }) => ( value={inventory}
<> onBlur={() => inventoryHelpers.setTouched()}
<InventoryLookup onChange={value => {
value={inventory} inventoryHelpers.setValue(value.id);
onBlur={() => { setInventory(value);
form.setFieldTouched('inventory'); }}
}} required
onChange={value => { touched={inventoryMeta.touched}
form.setValues({ error={inventoryMeta.error}
...form.values, />
inventory: value.id, {(inventoryMeta.touched || formikValues.ask_inventory_on_launch) &&
organizationId: value.organization, inventoryMeta.error && (
}); <div
setInventory(value); className="pf-c-form__helper-text pf-m-error"
}} aria-live="polite"
required >
touched={form.touched.inventory} {inventoryMeta.error}
error={form.errors.inventory} </div>
/>
{(form.touched.inventory ||
form.touched.ask_inventory_on_launch) &&
form.errors.inventory && (
<div
className="pf-c-form__helper-text pf-m-error"
aria-live="polite"
>
{form.errors.inventory}
</div>
)}
</>
)} )}
</Field>
</FieldWithPrompt> </FieldWithPrompt>
<Field name="project" validate={handleProjectValidation}>
{({ form }) => ( <ProjectLookup
<ProjectLookup value={project}
value={project} onBlur={() => projectHelpers.setTouched()}
onBlur={() => form.setFieldTouched('project')} tooltip={i18n._(t`Select the project containing the playbook
tooltip={i18n._(t`Select the project containing the playbook
you want this job to execute.`)} you want this job to execute.`)}
isValid={!form.touched.project || !form.errors.project} isValid={!projectMeta.touched || !projectMeta.error}
helperTextInvalid={form.errors.project} helperTextInvalid={projectMeta.error}
onChange={handleProjectUpdate} onChange={handleProjectUpdate}
required required
/> />
)} {project?.allow_override && (
</Field>
{project && project.allow_override && (
<FieldWithPrompt <FieldWithPrompt
fieldId="template-scm-branch" fieldId="template-scm-branch"
label={i18n._(t`SCM Branch`)} label={i18n._(t`SCM Branch`)}
promptId="template-ask-scm-branch-on-launch" promptId="template-ask-scm-branch-on-launch"
promptName="ask_scm_branch_on_launch" promptName="ask_scm_branch_on_launch"
> >
<Field name="scm_branch"> <TextInput
{({ field }) => { id="template-scm-branch"
return ( {...scmField}
<TextInput onChange={(value, event) => {
id="template-scm-branch" scmField.onChange(event);
{...field}
onChange={(value, event) => {
field.onChange(event);
}}
/>
);
}} }}
</Field> />
</FieldWithPrompt> </FieldWithPrompt>
)} )}
<Field <FormGroup
name="playbook" fieldId="template-playbook"
validate={required(i18n._(t`Select a value for this field`), i18n)} helperTextInvalid={playbookMeta.error}
onBlur={handleBlur} isRequired
label={i18n._(t`Playbook`)}
> >
{({ field, form }) => { <FieldTooltip
const isValid = !form.touched.playbook || !form.errors.playbook; content={i18n._(t`Select the playbook to be executed by this job.`)}
return ( />
<FormGroup <PlaybookSelect
fieldId="template-playbook" projectId={project?.id || projectField.value?.id}
helperTextInvalid={form.errors.playbook} isValid={!(playbookMeta.touched || playbookMeta.error)}
isRequired field={playbookField}
isValid={isValid} onBlur={() => playbookHelpers.setTouched()}
label={i18n._(t`Playbook`)} onError={setContentError}
> />
<FieldTooltip </FormGroup>
content={i18n._(
t`Select the playbook to be executed by this job.`
)}
/>
<PlaybookSelect
projectId={form.values.project}
isValid={isValid}
form={form}
field={field}
onBlur={() => form.setFieldTouched('playbook')}
onError={setContentError}
/>
</FormGroup>
);
}}
</Field>
<FormFullWidthLayout> <FormFullWidthLayout>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-credentials" fieldId="template-credentials"
@@ -328,36 +307,26 @@ function JobTemplateForm({
credential at run time. If you select credentials and check "Prompt on launch", the selected credential at run time. If you select credentials and check "Prompt on launch", the selected
credential(s) become the defaults that can be updated at run time.`)} credential(s) become the defaults that can be updated at run time.`)}
> >
<Field name="credentials" fieldId="template-credentials"> <MultiCredentialsLookup
{({ field }) => { value={credentialField.value}
return ( onChange={newCredentials =>
<MultiCredentialsLookup credentialHelpers.setValue(newCredentials)
value={field.value} }
onChange={newCredentials => onError={setContentError}
setFieldValue('credentials', newCredentials) />
}
onError={setContentError}
/>
);
}}
</Field>
</FieldWithPrompt> </FieldWithPrompt>
<Field name="labels"> <FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
{({ field }) => ( <FieldTooltip
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels"> content={i18n._(t`Optional labels that describe this job template,
<FieldTooltip
content={i18n._(t`Optional labels that describe this job template,
such as 'dev' or 'test'. Labels can be used to group and filter such as 'dev' or 'test'. Labels can be used to group and filter
job templates and completed jobs.`)} job templates and completed jobs.`)}
/> />
<LabelSelect <LabelSelect
value={field.value} value={labelsField.value}
onChange={labels => setFieldValue('labels', labels)} onChange={labels => labelsHelpers.setValue(labels)}
onError={setContentError} onError={setContentError}
/> />
</FormGroup> </FormGroup>
)}
</Field>
<VariablesField <VariablesField
id="template-variables" id="template-variables"
name="extra_vars" name="extra_vars"
@@ -394,20 +363,14 @@ function JobTemplateForm({
playbook. Multiple patterns are allowed. Refer to Ansible playbook. Multiple patterns are allowed. Refer to Ansible
documentation for more information and examples on patterns.`)} documentation for more information and examples on patterns.`)}
> >
<Field name="limit"> <TextInput
{({ form, field }) => { id="template-limit"
return ( {...limitField}
<TextInput isValid={!limitMeta.touched || !limitMeta.error}
id="template-limit" onChange={(value, event) => {
{...field} limitField.onChange(event);
isValid={!form.touched.job_type || !form.errors.job_type}
onChange={(value, event) => {
field.onChange(event);
}}
/>
);
}} }}
</Field> />
</FieldWithPrompt> </FieldWithPrompt>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-verbosity" fieldId="template-verbosity"
@@ -417,15 +380,11 @@ function JobTemplateForm({
tooltip={i18n._(t`Control the level of output ansible will tooltip={i18n._(t`Control the level of output ansible will
produce as the playbook executes.`)} produce as the playbook executes.`)}
> >
<Field name="verbosity"> <AnsibleSelect
{({ field }) => ( id="template-verbosity"
<AnsibleSelect data={verbosityOptions}
id="template-verbosity" {...verbosityField}
data={verbosityOptions} />
{...field}
/>
)}
</Field>
</FieldWithPrompt> </FieldWithPrompt>
<FormField <FormField
id="template-job-slicing" id="template-job-slicing"
@@ -456,32 +415,20 @@ function JobTemplateForm({
Ansible tasks, where supported. This is equivalent Ansible tasks, where supported. This is equivalent
to Ansible&#x2019s --diff mode.`)} to Ansible&#x2019s --diff mode.`)}
> >
<Field name="diff_mode"> <Switch
{({ form, field }) => { id="template-show-changes"
return ( label={diffModeField.value ? i18n._(t`On`) : i18n._(t`Off`)}
<Switch isChecked={diffModeField.value}
id="template-show-changes" onChange={checked => diffModeHelpers.setValue(checked)}
label={field.value ? i18n._(t`On`) : i18n._(t`Off`)} />
isChecked={field.value}
onChange={checked =>
form.setFieldValue(field.name, checked)
}
/>
);
}}
</Field>
</FieldWithPrompt> </FieldWithPrompt>
<FormFullWidthLayout> <FormFullWidthLayout>
<Field name="instanceGroups"> <InstanceGroupsLookup
{({ field, form }) => ( value={instanceGroupsField.value}
<InstanceGroupsLookup onChange={value => instanceGroupsHelpers.setValue(value)}
value={field.value} tooltip={i18n._(t`Select the Instance Groups for this Organization
onChange={value => form.setFieldValue(field.name, value)}
tooltip={i18n._(t`Select the Instance Groups for this Organization
to run on.`)} to run on.`)}
/> />
)}
</Field>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-tags" fieldId="template-tags"
label={i18n._(t`Job Tags`)} label={i18n._(t`Job Tags`)}
@@ -493,14 +440,10 @@ function JobTemplateForm({
Refer to Ansible Tower documentation for details on Refer to Ansible Tower documentation for details on
the usage of tags.`)} the usage of tags.`)}
> >
<Field name="job_tags"> <TagMultiSelect
{({ field, form }) => ( value={jobTagsField.value}
<TagMultiSelect onChange={value => jobTagsHelpers.setValue(value)}
value={field.value} />
onChange={value => form.setFieldValue(field.name, value)}
/>
)}
</Field>
</FieldWithPrompt> </FieldWithPrompt>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-skip-tags" fieldId="template-skip-tags"
@@ -513,14 +456,10 @@ function JobTemplateForm({
to Ansible Tower documentation for details on the usage to Ansible Tower documentation for details on the usage
of tags.`)} of tags.`)}
> >
<Field name="skip_tags"> <TagMultiSelect
{({ field, form }) => ( value={skipTagsField.value}
<TagMultiSelect onChange={value => skipTagsHelpers.setValue(value)}
value={field.value} />
onChange={value => form.setFieldValue(field.name, value)}
/>
)}
</Field>
</FieldWithPrompt> </FieldWithPrompt>
<FormGroup <FormGroup
fieldId="template-option-checkboxes" fieldId="template-option-checkboxes"
@@ -636,10 +575,6 @@ const FormikApp = withFormik({
}, },
} = template; } = template;
const hasInventory = summary_fields.inventory
? summary_fields.inventory.organization_id
: null;
return { return {
ask_credential_on_launch: template.ask_credential_on_launch || false, ask_credential_on_launch: template.ask_credential_on_launch || false,
ask_diff_mode_on_launch: template.ask_diff_mode_on_launch || false, ask_diff_mode_on_launch: template.ask_diff_mode_on_launch || false,
@@ -655,7 +590,7 @@ const FormikApp = withFormik({
description: template.description || '', description: template.description || '',
job_type: template.job_type || 'run', job_type: template.job_type || 'run',
inventory: template.inventory || null, inventory: template.inventory || null,
project: template.project || '', project: template.project || null,
scm_branch: template.scm_branch || '', scm_branch: template.scm_branch || '',
playbook: template.playbook || '', playbook: template.playbook || '',
labels: summary_fields.labels.results || [], labels: summary_fields.labels.results || [],
@@ -672,7 +607,6 @@ const FormikApp = withFormik({
allow_simultaneous: template.allow_simultaneous || false, allow_simultaneous: template.allow_simultaneous || false,
use_fact_cache: template.use_fact_cache || false, use_fact_cache: template.use_fact_cache || false,
host_config_key: template.host_config_key || '', host_config_key: template.host_config_key || '',
organizationId: hasInventory,
initialInstanceGroups: [], initialInstanceGroups: [],
instanceGroups: [], instanceGroups: [],
credentials: summary_fields.credentials || [], credentials: summary_fields.credentials || [],

View File

@@ -14,7 +14,7 @@ describe('<JobTemplateForm />', () => {
description: 'Bar', description: 'Bar',
job_type: 'run', job_type: 'run',
inventory: 2, inventory: 2,
project: 3, project: { id: 3, summary_fields: { organization: { id: 1 } } },
playbook: 'Baz', playbook: 'Baz',
type: 'job_template', type: 'job_template',
scm_branch: 'Foo', scm_branch: 'Foo',

View File

@@ -5,15 +5,7 @@ import { t } from '@lingui/macro';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import { ProjectsAPI } from '@api'; import { ProjectsAPI } from '@api';
function PlaybookSelect({ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
projectId,
isValid,
form,
field,
onBlur,
onError,
i18n,
}) {
const [options, setOptions] = useState([]); const [options, setOptions] = useState([]);
useEffect(() => { useEffect(() => {
@@ -47,7 +39,6 @@ function PlaybookSelect({
id="template-playbook" id="template-playbook"
data={options} data={options}
isValid={isValid} isValid={isValid}
form={form}
{...field} {...field}
onBlur={onBlur} onBlur={onBlur}
/> />

View File

@@ -70,7 +70,7 @@ export const JobTemplate = shape({
inventory: number, inventory: number,
job_type: oneOf(['run', 'check']), job_type: oneOf(['run', 'check']),
playbook: string, playbook: string,
project: number, project: shape({}),
}); });
export const Inventory = shape({ export const Inventory = shape({