1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-27 00:55:06 +03:00

Merge pull request #8104 from mabashian/4254-auto-pop-lookup

Auto populate various required lookups on various forms

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-09-21 21:37:02 +00:00 committed by GitHub
commit 31cd36b768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 657 additions and 282 deletions

View File

@ -10,6 +10,7 @@ import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
import { FieldTooltip } from '../FormField';
import Lookup from './Lookup';
import OptionsList from '../OptionsList';
import useAutoPopulateLookup from '../../util/useAutoPopulateLookup';
import useRequest from '../../util/useRequest';
import LookupErrorMessage from './shared/LookupErrorMessage';
@ -34,7 +35,9 @@ function CredentialLookup({
i18n,
tooltip,
isDisabled,
autoPopulate,
}) {
const autoPopulateLookup = useAutoPopulateLookup(onChange);
const {
result: { count, credentials, relatedSearchableKeys, searchableKeys },
error,
@ -62,6 +65,11 @@ function CredentialLookup({
),
CredentialsAPI.readOptions,
]);
if (autoPopulate) {
autoPopulateLookup(data.results);
}
return {
count: data.count,
credentials: data.results,
@ -73,6 +81,8 @@ function CredentialLookup({
).filter(key => actionsResponse.data?.actions?.GET[key]?.filterable),
};
}, [
autoPopulate,
autoPopulateLookup,
credentialTypeId,
credentialTypeKind,
credentialTypeNamespace,
@ -182,6 +192,8 @@ CredentialLookup.propTypes = {
onChange: func.isRequired,
required: bool,
value: Credential,
isDisabled: bool,
autoPopulate: bool,
};
CredentialLookup.defaultProps = {
@ -192,6 +204,8 @@ CredentialLookup.defaultProps = {
onBlur: () => {},
required: false,
value: null,
isDisabled: false,
autoPopulate: false,
};
export { CredentialLookup as _CredentialLookup };

View File

@ -88,4 +88,66 @@ describe('CredentialLookup', () => {
expect(_CredentialLookup.defaultProps.onBlur).toBeInstanceOf(Function);
expect(_CredentialLookup.defaultProps.onBlur).not.toThrow();
});
test('should auto-select credential when only one available and autoPopulate prop is true', async () => {
CredentialsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
const onChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<CredentialLookup
autoPopulate
credentialTypeId={1}
label="Foo"
onChange={onChange}
/>
);
});
expect(onChange).toHaveBeenCalledWith({ id: 1 });
});
test('should not auto-select credential when autoPopulate prop is false', async () => {
CredentialsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
const onChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<CredentialLookup
credentialTypeId={1}
label="Foo"
onChange={onChange}
/>
);
});
expect(onChange).not.toHaveBeenCalled();
});
test('should not auto-select credential when multiple available', async () => {
CredentialsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }, { id: 2 }],
count: 2,
},
});
const onChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<CredentialLookup
credentialTypeId={1}
label="Foo"
autoPopulate
onChange={onChange}
/>
);
});
expect(onChange).not.toHaveBeenCalled();
});
});

View File

@ -8,6 +8,7 @@ import { OrganizationsAPI } from '../../api';
import { Organization } from '../../types';
import { getQSConfig, parseQueryString } from '../../util/qs';
import useRequest from '../../util/useRequest';
import useAutoPopulateLookup from '../../util/useAutoPopulateLookup';
import OptionsList from '../OptionsList';
import Lookup from './Lookup';
import LookupErrorMessage from './shared/LookupErrorMessage';
@ -27,7 +28,10 @@ function OrganizationLookup({
required,
value,
history,
autoPopulate,
}) {
const autoPopulateLookup = useAutoPopulateLookup(onChange);
const {
result: { itemCount, organizations, relatedSearchableKeys, searchableKeys },
error: contentError,
@ -39,6 +43,11 @@ function OrganizationLookup({
OrganizationsAPI.read(params),
OrganizationsAPI.readOptions(),
]);
if (autoPopulate) {
autoPopulateLookup(response.data.results);
}
return {
organizations: response.data.results,
itemCount: response.data.count,
@ -49,7 +58,7 @@ function OrganizationLookup({
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
};
}, [history.location.search]),
}, [autoPopulate, autoPopulateLookup, history.location.search]),
{
organizations: [],
itemCount: 0,
@ -129,6 +138,7 @@ OrganizationLookup.propTypes = {
onChange: func.isRequired,
required: bool,
value: Organization,
autoPopulate: bool,
};
OrganizationLookup.defaultProps = {
@ -137,6 +147,7 @@ OrganizationLookup.defaultProps = {
onBlur: () => {},
required: false,
value: null,
autoPopulate: false,
};
export { OrganizationLookup as _OrganizationLookup };

View File

@ -48,4 +48,50 @@ describe('OrganizationLookup', () => {
expect(_OrganizationLookup.defaultProps.onBlur).toBeInstanceOf(Function);
expect(_OrganizationLookup.defaultProps.onBlur).not.toThrow();
});
test('should auto-select organization when only one available and autoPopulate prop is true', async () => {
OrganizationsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
const onChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<OrganizationLookup autoPopulate onChange={onChange} />
);
});
expect(onChange).toHaveBeenCalledWith({ id: 1 });
});
test('should not auto-select organization when autoPopulate prop is false', async () => {
OrganizationsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
const onChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(<OrganizationLookup onChange={onChange} />);
});
expect(onChange).not.toHaveBeenCalled();
});
test('should not auto-select organization when multiple available', async () => {
OrganizationsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }, { id: 2 }],
count: 2,
},
});
const onChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<OrganizationLookup autoPopulate onChange={onChange} />
);
});
expect(onChange).not.toHaveBeenCalled();
});
});

View File

@ -8,6 +8,7 @@ import { ProjectsAPI } from '../../api';
import { Project } from '../../types';
import { FieldTooltip } from '../FormField';
import OptionsList from '../OptionsList';
import useAutoPopulateLookup from '../../util/useAutoPopulateLookup';
import useRequest from '../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../util/qs';
import Lookup from './Lookup';
@ -21,7 +22,7 @@ const QS_CONFIG = getQSConfig('project', {
function ProjectLookup({
helperTextInvalid,
autocomplete,
autoPopulate,
i18n,
isValid,
onChange,
@ -31,6 +32,7 @@ function ProjectLookup({
onBlur,
history,
}) {
const autoPopulateLookup = useAutoPopulateLookup(onChange);
const {
result: { projects, count, relatedSearchableKeys, searchableKeys, canEdit },
request: fetchProjects,
@ -43,8 +45,8 @@ function ProjectLookup({
ProjectsAPI.read(params),
ProjectsAPI.readOptions(),
]);
if (data.count === 1 && autocomplete) {
autocomplete(data.results[0]);
if (autoPopulate) {
autoPopulateLookup(data.results);
}
return {
count: data.count,
@ -57,7 +59,7 @@ function ProjectLookup({
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
canEdit: Boolean(actionsResponse.data.actions.POST),
};
}, [history.location.search, autocomplete]),
}, [autoPopulate, autoPopulateLookup, history.location.search]),
{
count: 0,
projects: [],
@ -151,7 +153,7 @@ function ProjectLookup({
}
ProjectLookup.propTypes = {
autocomplete: func,
autoPopulate: bool,
helperTextInvalid: node,
isValid: bool,
onBlur: func,
@ -162,7 +164,7 @@ ProjectLookup.propTypes = {
};
ProjectLookup.defaultProps = {
autocomplete: () => {},
autoPopulate: false,
helperTextInvalid: '',
isValid: true,
onBlur: () => {},

View File

@ -1,28 +1,38 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import { sleep } from '../../../testUtils/testUtils';
import { ProjectsAPI } from '../../api';
import ProjectLookup from './ProjectLookup';
jest.mock('../../api');
describe('<ProjectLookup />', () => {
test('should auto-select project when only one available', async () => {
test('should auto-select project when only one available and autoPopulate prop is true', async () => {
ProjectsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
const autocomplete = jest.fn();
const onChange = jest.fn();
await act(async () => {
mountWithContexts(
<ProjectLookup autocomplete={autocomplete} onChange={() => {}} />
);
mountWithContexts(<ProjectLookup autoPopulate onChange={onChange} />);
});
await sleep(0);
expect(autocomplete).toHaveBeenCalledWith({ id: 1 });
expect(onChange).toHaveBeenCalledWith({ id: 1 });
});
test('should not auto-select project when autoPopulate prop is false', async () => {
ProjectsAPI.read.mockReturnValue({
data: {
results: [{ id: 1 }],
count: 1,
},
});
const onChange = jest.fn();
await act(async () => {
mountWithContexts(<ProjectLookup onChange={onChange} />);
});
expect(onChange).not.toHaveBeenCalled();
});
test('should not auto-select project when multiple available', async () => {
@ -32,13 +42,10 @@ describe('<ProjectLookup />', () => {
count: 2,
},
});
const autocomplete = jest.fn();
const onChange = jest.fn();
await act(async () => {
mountWithContexts(
<ProjectLookup autocomplete={autocomplete} onChange={() => {}} />
);
mountWithContexts(<ProjectLookup autoPopulate onChange={onChange} />);
});
await sleep(0);
expect(autocomplete).not.toHaveBeenCalled();
expect(onChange).not.toHaveBeenCalled();
});
});

View File

@ -1,8 +1,8 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, useField } from 'formik';
import { Formik, useField, useFormikContext } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core';
import PropTypes from 'prop-types';
@ -18,10 +18,12 @@ import AnsibleSelect from '../../../components/AnsibleSelect';
function ApplicationFormFields({
i18n,
application,
authorizationOptions,
clientTypeOptions,
}) {
const match = useRouteMatch();
const { setFieldValue } = useFormikContext();
const [organizationField, organizationMeta, organizationHelpers] = useField({
name: 'organization',
validate: required(null, i18n),
@ -40,6 +42,13 @@ function ApplicationFormFields({
validate: required(null, i18n),
});
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
return (
<>
<FormField
@ -60,11 +69,10 @@ function ApplicationFormFields({
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value);
}}
onChange={onOrganizationChange}
value={organizationField.value}
required
autoPopulate={!application?.id}
/>
<FormGroup
fieldId="authType"
@ -166,6 +174,7 @@ function ApplicationForm({
<FormColumnLayout>
<ApplicationFormFields
formik={formik}
application={application}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
i18n={i18n}

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Formik, useField } from 'formik';
import React, { useCallback, useState } from 'react';
import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { arrayOf, func, object, shape } from 'prop-types';
@ -21,6 +21,7 @@ function CredentialFormFields({
formik,
initialValues,
}) {
const { setFieldValue } = useFormikContext();
const [orgField, orgMeta, orgHelpers] = useField('organization');
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
name: 'credential_type',
@ -76,6 +77,13 @@ function CredentialFormFields({
);
};
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
return (
<>
<FormField
@ -96,9 +104,7 @@ function CredentialFormFields({
helperTextInvalid={orgMeta.error}
isValid={!orgMeta.touched || !orgMeta.error}
onBlur={() => orgHelpers.setTouched()}
onChange={value => {
orgHelpers.setValue(value);
}}
onChange={onOrganizationChange}
value={orgField.value}
touched={orgMeta.touched}
error={orgMeta.error}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Formik, useField } from 'formik';
import React, { useCallback } from 'react';
import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { func, number, shape } from 'prop-types';
@ -17,18 +17,29 @@ import {
FormFullWidthLayout,
} from '../../../components/FormLayout';
function InventoryFormFields({ i18n, credentialTypeId }) {
function InventoryFormFields({ i18n, credentialTypeId, inventory }) {
const { setFieldValue } = useFormikContext();
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 [instanceGroupsField, , instanceGroupsHelpers] = useField(
'instanceGroups'
);
const [insightsCredentialField] = useField('insights_credential');
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
const onCredentialChange = useCallback(
value => {
setFieldValue('insights_credential', value);
},
[setFieldValue]
);
const insightsCredentialFieldArr = useField('insights_credential');
const insightsCredentialField = insightsCredentialFieldArr[0];
const insightsCredentialHelpers = insightsCredentialFieldArr[2];
return (
<>
<FormField
@ -49,18 +60,17 @@ function InventoryFormFields({ i18n, credentialTypeId }) {
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value);
}}
onChange={onOrganizationChange}
value={organizationField.value}
touched={organizationMeta.touched}
error={organizationMeta.error}
required
autoPopulate={!inventory?.id}
/>
<CredentialLookup
label={i18n._(t`Insights Credential`)}
credentialTypeId={credentialTypeId}
onChange={value => insightsCredentialHelpers.setValue(value)}
onChange={onCredentialChange}
value={insightsCredentialField.value}
/>
<InstanceGroupsLookup
@ -115,7 +125,7 @@ function InventoryForm({
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<InventoryFormFields {...rest} />
<InventoryFormFields {...rest} inventory={inventory} />
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={onCancel}

View File

@ -42,7 +42,7 @@ const buildSourceChoiceOptions = options => {
return sourceChoices.filter(({ key }) => key !== 'file');
};
const InventorySourceFormFields = ({ sourceOptions, i18n }) => {
const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => {
const {
values,
initialValues,
@ -170,16 +170,67 @@ const InventorySourceFormFields = ({ sourceOptions, i18n }) => {
<FormColumnLayout>
{
{
azure_rm: <AzureSubForm sourceOptions={sourceOptions} />,
azure_rm: (
<AzureSubForm
autoPopulateCredential={
!source?.id || source?.source !== 'azure_rm'
}
sourceOptions={sourceOptions}
/>
),
cloudforms: <CloudFormsSubForm />,
ec2: <EC2SubForm sourceOptions={sourceOptions} />,
gce: <GCESubForm sourceOptions={sourceOptions} />,
openstack: <OpenStackSubForm />,
rhv: <VirtualizationSubForm />,
satellite6: <SatelliteSubForm />,
scm: <SCMSubForm />,
tower: <TowerSubForm />,
vmware: <VMwareSubForm sourceOptions={sourceOptions} />,
gce: (
<GCESubForm
autoPopulateCredential={
!source?.id || source?.source !== 'gce'
}
sourceOptions={sourceOptions}
/>
),
openstack: (
<OpenStackSubForm
autoPopulateCredential={
!source?.id || source?.source !== 'openstack'
}
/>
),
rhv: (
<VirtualizationSubForm
autoPopulateCredential={
!source?.id || source?.source !== 'rhv'
}
/>
),
satellite6: (
<SatelliteSubForm
autoPopulateCredential={
!source?.id || source?.source !== 'satellite6'
}
/>
),
scm: (
<SCMSubForm
autoPopulateProject={
!source?.id || source?.source !== 'scm'
}
/>
),
tower: (
<TowerSubForm
autoPopulateCredential={
!source?.id || source?.source !== 'tower'
}
/>
),
vmware: (
<VMwareSubForm
autoPopulateCredential={
!source?.id || source?.source !== 'vmware'
}
sourceOptions={sourceOptions}
/>
),
}[sourceField.value]
}
</FormColumnLayout>
@ -255,6 +306,7 @@ const InventorySourceForm = ({
<InventorySourceFormFields
formik={formik}
i18n={i18n}
source={source}
sourceOptions={sourceOptions}
/>
{submitError && <FormSubmitError error={submitError} />}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -12,11 +12,19 @@ import {
HostFilterField,
} from './SharedFields';
const AzureSubForm = ({ i18n }) => {
const AzureSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -25,11 +33,10 @@ const AzureSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -13,10 +13,18 @@ import {
} from './SharedFields';
const CloudFormsSubForm = ({ i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -25,9 +33,7 @@ const CloudFormsSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
/>

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -13,16 +13,23 @@ import {
} from './SharedFields';
const EC2SubForm = ({ i18n }) => {
const [credentialField, , credentialHelpers] = useField('credential');
const { setFieldValue } = useFormikContext();
const [credentialField] = useField('credential');
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
credentialTypeNamespace="aws"
label={i18n._(t`Credential`)}
value={credentialField.value}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -11,11 +11,19 @@ import {
HostFilterField,
} from './SharedFields';
const GCESubForm = ({ i18n }) => {
const GCESubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -24,11 +32,10 @@ const GCESubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -12,11 +12,19 @@ import {
HostFilterField,
} from './SharedFields';
const OpenStackSubForm = ({ i18n }) => {
const OpenStackSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -25,11 +33,10 @@ const OpenStackSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
import { useField } from 'formik';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core';
@ -20,8 +20,9 @@ import {
HostFilterField,
} from './SharedFields';
const SCMSubForm = ({ i18n }) => {
const [credentialField, , credentialHelpers] = useField('credential');
const SCMSubForm = ({ autoPopulateProject, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField] = useField('credential');
const [projectField, projectMeta, projectHelpers] = useField({
name: 'source_project',
validate: required(i18n._(t`Select a value for this field`), i18n),
@ -51,21 +52,18 @@ const SCMSubForm = ({ i18n }) => {
const handleProjectUpdate = useCallback(
value => {
sourcePathHelpers.setValue('');
projectHelpers.setValue(value);
setFieldValue('source_path', '');
setFieldValue('source_project', value);
fetchSourcePath(value.id);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
[fetchSourcePath, setFieldValue]
);
const handleProjectAutocomplete = useCallback(
val => {
projectHelpers.setValue(val);
if (!projectMeta.initialValue) {
fetchSourcePath(val.id);
}
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
[setFieldValue]
);
return (
@ -74,18 +72,16 @@ const SCMSubForm = ({ i18n }) => {
credentialTypeKind="cloud"
label={i18n._(t`Credential`)}
value={credentialField.value}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
/>
<ProjectLookup
autocomplete={handleProjectAutocomplete}
value={projectField.value}
isValid={!projectMeta.touched || !projectMeta.error}
helperTextInvalid={projectMeta.error}
onBlur={() => projectHelpers.setTouched()}
onChange={handleProjectUpdate}
required
autoPopulate={autoPopulateProject}
/>
<FormGroup
fieldId="source_path"

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -12,11 +12,19 @@ import {
HostFilterField,
} from './SharedFields';
const SatelliteSubForm = ({ i18n }) => {
const SatelliteSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -25,11 +33,10 @@ const SatelliteSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -11,11 +11,19 @@ import {
HostFilterField,
} from './SharedFields';
const TowerSubForm = ({ i18n }) => {
const TowerSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -24,11 +32,10 @@ const TowerSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -12,11 +12,19 @@ import {
HostFilterField,
} from './SharedFields';
const VMwareSubForm = ({ i18n }) => {
const VMwareSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -25,11 +33,10 @@ const VMwareSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useField } from 'formik';
import React, { useCallback } from 'react';
import { useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
@ -11,11 +11,19 @@ import {
HostFilterField,
} from './SharedFields';
const VirtualizationSubForm = ({ i18n }) => {
const VirtualizationSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext();
const [credentialField, credentialMeta, credentialHelpers] = useField(
'credential'
);
const handleCredentialUpdate = useCallback(
value => {
setFieldValue('credential', value);
},
[setFieldValue]
);
return (
<>
<CredentialLookup
@ -24,11 +32,10 @@ const VirtualizationSubForm = ({ i18n }) => {
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={value => {
credentialHelpers.setValue(value);
}}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
/>
<VerbosityField />
<HostFilterField />

View File

@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react';
import { Formik, useField } from 'formik';
import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { func, shape, object, arrayOf } from 'prop-types';
@ -20,7 +20,8 @@ import useRequest from '../../../util/useRequest';
import { required } from '../../../util/validators';
import { InventoriesAPI } from '../../../api';
const SmartInventoryFormFields = withI18n()(({ i18n }) => {
const SmartInventoryFormFields = withI18n()(({ i18n, inventory }) => {
const { setFieldValue } = useFormikContext();
const [organizationField, organizationMeta, organizationHelpers] = useField({
name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n),
@ -32,6 +33,12 @@ const SmartInventoryFormFields = withI18n()(({ i18n }) => {
name: 'host_filter',
validate: required(null, i18n),
});
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
return (
<>
@ -53,11 +60,10 @@ const SmartInventoryFormFields = withI18n()(({ i18n }) => {
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value);
}}
onChange={onOrganizationChange}
value={organizationField.value}
required
autoPopulate={!inventory?.id}
/>
<HostFilterLookup
value={hostFilterField.value}
@ -144,7 +150,7 @@ function SmartInventoryForm({
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<SmartInventoryFormFields />
<SmartInventoryFormFields inventory={inventory} />
{submitError && <FormSubmitError error={submitError} />}
<FormActionGroup
onCancel={onCancel}

View File

@ -1,9 +1,9 @@
/* eslint no-nested-ternary: 0 */
import React, { useState, useEffect } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, useField } from 'formik';
import { Formik, useField, useFormikContext } from 'formik';
import { Form, FormGroup, Title } from '@patternfly/react-core';
import { Config } from '../../../contexts/Config';
import AnsibleSelect from '../../../components/AnsibleSelect';
@ -69,6 +69,7 @@ const fetchCredentials = async credential => {
};
function ProjectFormFields({
project,
project_base_dir,
project_local_paths,
formik,
@ -91,6 +92,8 @@ function ProjectFormFields({
scm_update_cache_timeout: 0,
};
const { setFieldValue } = useFormikContext();
const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({
name: 'scm_type',
validate: required(i18n._(t`Set a value for this field`), i18n),
@ -133,15 +136,25 @@ function ProjectFormFields({
});
};
const handleCredentialSelection = (type, value) => {
setCredentials({
...credentials,
[type]: {
...credentials[type],
value,
},
});
};
const handleCredentialSelection = useCallback(
(type, value) => {
setCredentials({
...credentials,
[type]: {
...credentials[type],
value,
},
});
},
[credentials, setCredentials]
);
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
return (
<>
@ -163,11 +176,10 @@ function ProjectFormFields({
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value);
}}
onChange={onOrganizationChange}
value={organizationField.value}
required
autoPopulate={!project?.id}
/>
<FormGroup
fieldId="project-scm-type"
@ -253,6 +265,9 @@ function ProjectFormFields({
credential={credentials.insights}
onCredentialSelection={handleCredentialSelection}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
autoPopulateCredential={
!project?.id || project?.scm_type !== 'insights'
}
/>
),
}[formik.values.scm_type]
@ -379,6 +394,7 @@ function ProjectForm({ i18n, project, submitError, ...props }) {
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<ProjectFormFields
project={project}
project_base_dir={project_base_dir}
project_local_paths={project_local_paths}
formik={formik}

View File

@ -173,7 +173,7 @@ describe('<ProjectForm />', () => {
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
act(() => {
await act(async () => {
wrapper.find('OrganizationLookup').invoke('onBlur')();
wrapper.find('OrganizationLookup').invoke('onChange')({
id: 1,

View File

@ -1,7 +1,7 @@
import React from 'react';
import React, { useCallback } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { useField, useFormikContext } from 'formik';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
import { required } from '../../../../util/validators';
import { ScmTypeOptions } from './SharedFields';
@ -11,13 +11,21 @@ const InsightsSubForm = ({
credential,
onCredentialSelection,
scmUpdateOnLaunch,
autoPopulateCredential,
}) => {
const credFieldArr = useField({
const { setFieldValue } = useFormikContext();
const [, credMeta, credHelpers] = useField({
name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const credMeta = credFieldArr[1];
const credHelpers = credFieldArr[2];
const onCredentialChange = useCallback(
value => {
onCredentialSelection('insights', value);
setFieldValue('credential', value.id);
},
[onCredentialSelection, setFieldValue]
);
return (
<>
@ -27,12 +35,10 @@ const InsightsSubForm = ({
helperTextInvalid={credMeta.error}
isValid={!credMeta.touched || !credMeta.error}
onBlur={() => credHelpers.setTouched()}
onChange={value => {
onCredentialSelection('insights', value);
credHelpers.setValue(value.id);
}}
onChange={onCredentialChange}
value={credential.value}
required
autoPopulate={autoPopulateCredential}
/>
<ScmTypeOptions hideAllowOverride scmUpdateOnLaunch={scmUpdateOnLaunch} />
</>

View File

@ -1,7 +1,7 @@
import React from 'react';
import React, { useCallback } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { useFormikContext } from 'formik';
import { FormGroup, Title } from '@patternfly/react-core';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
import FormField, { CheckboxField } from '../../../../components/FormField';
@ -39,17 +39,22 @@ export const BranchFormField = withI18n()(({ i18n, label }) => (
export const ScmCredentialFormField = withI18n()(
({ i18n, credential, onCredentialSelection }) => {
const credHelpers = useField('credential')[2];
const { setFieldValue } = useFormikContext();
const onCredentialChange = useCallback(
value => {
onCredentialSelection('scm', value);
setFieldValue('credential', value ? value.id : '');
},
[onCredentialSelection, setFieldValue]
);
return (
<CredentialLookup
credentialTypeId={credential.typeId}
label={i18n._(t`Source Control Credential`)}
value={credential.value}
onChange={value => {
onCredentialSelection('scm', value);
credHelpers.setValue(value ? value.id : '');
}}
onChange={onCredentialChange}
/>
);
}

View File

@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, useField } from 'formik';
import { Formik, useField, useFormikContext } from 'formik';
import { Form } from '@patternfly/react-core';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
import FormField, { FormSubmitError } from '../../../components/FormField';
@ -10,17 +10,23 @@ import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
import { required } from '../../../util/validators';
import { FormColumnLayout } from '../../../components/FormLayout';
function TeamFormFields(props) {
const { team, i18n } = props;
function TeamFormFields({ team, i18n }) {
const { setFieldValue } = useFormikContext();
const [organization, setOrganization] = useState(
team.summary_fields ? team.summary_fields.organization : null
);
const orgFieldArr = useField({
const [, orgMeta, orgHelpers] = useField({
name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
const orgMeta = orgFieldArr[1];
const orgHelpers = orgFieldArr[2];
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value.id);
setOrganization(value);
},
[setFieldValue]
);
return (
<>
@ -42,12 +48,10 @@ function TeamFormFields(props) {
helperTextInvalid={orgMeta.error}
isValid={!orgMeta.touched || !orgMeta.error}
onBlur={() => orgHelpers.setTouched('organization')}
onChange={value => {
orgHelpers.setValue(value.id);
setOrganization(value);
}}
onChange={onOrganizationChange}
value={organization}
required
autoPopulate={!team?.id}
/>
</>
);

View File

@ -74,91 +74,119 @@ describe('<WorkflowJobTemplate/>', () => {
data: { results: [{ id: 1, name: 'Org Foo' }] },
});
});
beforeEach(() => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
data: { actions: { PUT: {} } },
});
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
act(() => {
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)}
/>,
{
context: {
router: {
history,
},
},
}
);
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('calls api to get workflow job template data', async () => {
expect(wrapper.find('WorkflowJobTemplate').length).toBe(1);
expect(WorkflowJobTemplatesAPI.readDetail).toBeCalledWith('1');
wrapper.update();
await sleep(0);
expect(WorkflowJobTemplatesAPI.readWebhookKey).toBeCalledWith('1');
expect(WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions).toBeCalled();
describe('User can PUT', () => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
data: { actions: { PUT: {} } },
});
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)}
/>,
{
context: {
router: {
history,
},
},
}
);
});
});
test('calls api to get workflow job template data', async () => {
expect(wrapper.find('WorkflowJobTemplate').length).toBe(1);
expect(WorkflowJobTemplatesAPI.readDetail).toBeCalledWith('1');
wrapper.update();
await sleep(0);
expect(WorkflowJobTemplatesAPI.readWebhookKey).toBeCalledWith('1');
expect(
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions
).toBeCalled();
expect(CredentialsAPI.readDetail).toBeCalledWith(1234567);
expect(OrganizationsAPI.read).toBeCalledWith({
page_size: 1,
role_level: 'notification_admin_role',
expect(CredentialsAPI.readDetail).toBeCalledWith(1234567);
expect(OrganizationsAPI.read).toBeCalledWith({
page_size: 1,
role_level: 'notification_admin_role',
});
});
test('renders proper tabs', async () => {
const tabs = [
'Details',
'Access',
'Notifications',
'Schedules',
'Visualizer',
'Completed Jobs',
'Survey',
];
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.update();
wrapper.find('TabContainer').forEach(tc => {
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`)));
});
});
test('Does not render Notifications tab', async () => {
OrganizationsAPI.read.mockResolvedValue({
data: { results: [] },
});
const tabs = [
'Details',
'Access',
'Schedules',
'Visualizer',
'Completed Jobs',
'Survey',
];
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.update();
wrapper.find('TabContainer').forEach(tc => {
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`)));
});
});
});
test('renders proper tabs', async () => {
const tabs = [
'Details',
'Access',
'Notifications',
'Schedules',
'Visualizer',
'Completed Jobs',
'Survey',
];
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.update();
wrapper.find('TabContainer').forEach(tc => {
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`)));
describe('User cannot PUT', () => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValueOnce(
{
data: { actions: {} },
}
);
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)}
/>,
{
context: {
router: {
history,
},
},
}
);
});
});
});
test('Does not render Notifications tab', async () => {
OrganizationsAPI.read.mockResolvedValue({
data: { results: [] },
test('should not call for webhook key', async () => {
expect(WorkflowJobTemplatesAPI.readWebhookKey).not.toBeCalled();
});
const tabs = [
'Details',
'Access',
'Schedules',
'Visualizer',
'Completed Jobs',
'Survey',
];
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.update();
wrapper.find('TabContainer').forEach(tc => {
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`)));
});
});
test('should not call for webhook key', async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValueOnce(
{
data: { actions: {} },
}
);
expect(WorkflowJobTemplatesAPI.readWebhookKey).not.toBeCalled();
});
});

View File

@ -146,18 +146,11 @@ function JobTemplateForm({
const handleProjectUpdate = useCallback(
value => {
playbookHelpers.setValue(0);
scmHelpers.setValue('');
projectHelpers.setValue(value);
setFieldValue('playbook', 0);
setFieldValue('scm_branch', '');
setFieldValue('project', value);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
);
const handleProjectAutocomplete = useCallback(
val => {
projectHelpers.setValue(val);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
[setFieldValue]
);
const jobTypeOptions = [
@ -270,8 +263,8 @@ function JobTemplateForm({
isValid={!projectMeta.touched || !projectMeta.error}
helperTextInvalid={projectMeta.error}
onChange={handleProjectUpdate}
autocomplete={handleProjectAutocomplete}
required
autoPopulate={!template?.id}
/>
{projectField.value?.allow_override && (
<FieldWithPrompt

View File

@ -9,7 +9,7 @@ import {
InputGroup,
Button,
} from '@patternfly/react-core';
import { useField } from 'formik';
import { useField, useFormikContext } from 'formik';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import useRequest from '../../../util/useRequest';
@ -24,6 +24,7 @@ import {
} from '../../../api';
function WebhookSubForm({ i18n, templateType }) {
const { setFieldValue } = useFormikContext();
const { id } = useParams();
const { pathname } = useLocation();
const { origin } = document.location;
@ -82,6 +83,14 @@ function WebhookSubForm({ i18n, templateType }) {
const changeWebhookKey = async () => {
await fetchWebhookKey();
};
const onCredentialChange = useCallback(
value => {
setFieldValue('webhook_credential', value || null);
},
[setFieldValue]
);
const isUpdateKeyDisabled =
pathname.endsWith('/add') ||
webhookKeyMeta.initialValue ===
@ -211,9 +220,7 @@ function WebhookSubForm({ i18n, templateType }) {
t`Optionally select the credential to use to send status updates back to the webhook service.`
)}
credentialTypeId={credTypeId}
onChange={value => {
webhookCredentialHelpers.setValue(value || null);
}}
onChange={onCredentialChange}
isValid={!webhookCredentialMeta.error}
helperTextInvalid={webhookCredentialMeta.error}
value={webhookCredentialField.value}

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { t } from '@lingui/macro';
import PropTypes, { shape } from 'prop-types';
import { withI18n } from '@lingui/react';
import { useField, withFormik } from 'formik';
import { useField, useFormikContext, withFormik } from 'formik';
import {
Form,
FormGroup,
@ -43,6 +43,7 @@ function WorkflowJobTemplateForm({
i18n,
submitError,
}) {
const { setFieldValue } = useFormikContext();
const [enableWebhooks, setEnableWebhooks] = useState(
Boolean(template.webhook_service)
);
@ -53,9 +54,7 @@ function WorkflowJobTemplateForm({
);
const [labelsField, , labelsHelpers] = useField('labels');
const [limitField, limitMeta, limitHelpers] = useField('limit');
const [organizationField, organizationMeta, organizationHelpers] = useField(
'organization'
);
const [organizationField, organizationMeta] = useField('organization');
const [scmField, , scmHelpers] = useField('scm_branch');
const [, webhookServiceMeta, webhookServiceHelpers] = useField(
'webhook_service'
@ -81,6 +80,13 @@ function WorkflowJobTemplateForm({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enableWebhooks]);
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
if (hasContentError) {
return <ContentError error={hasContentError} />;
}
@ -104,9 +110,7 @@ function WorkflowJobTemplateForm({
/>
<OrganizationLookup
helperTextInvalid={organizationMeta.error}
onChange={value => {
organizationHelpers.setValue(value || null);
}}
onChange={onOrganizationChange}
value={organizationField.value}
isValid={!organizationMeta.error}
/>

View File

@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, useField } from 'formik';
import { Formik, useField, useFormikContext } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core';
import AnsibleSelect from '../../../components/AnsibleSelect';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
@ -16,6 +16,7 @@ import { FormColumnLayout } from '../../../components/FormLayout';
function UserFormFields({ user, i18n }) {
const [organization, setOrganization] = useState(null);
const { setFieldValue } = useFormikContext();
const userTypeOptions = [
{
@ -38,17 +39,23 @@ function UserFormFields({ user, i18n }) {
},
];
const organizationFieldArr = useField({
const [, organizationMeta, organizationHelpers] = useField({
name: 'organization',
validate: !user.id
? required(i18n._(t`Select a value for this field`), i18n)
: () => undefined,
});
const organizationMeta = organizationFieldArr[1];
const organizationHelpers = organizationFieldArr[2];
const [userTypeField, userTypeMeta] = useField('user_type');
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value.id);
setOrganization(value);
},
[setFieldValue]
);
return (
<>
<FormField
@ -105,12 +112,10 @@ function UserFormFields({ user, i18n }) {
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value.id);
setOrganization(value);
}}
onChange={onOrganizationChange}
value={organization}
required
autoPopulate={!user?.id}
/>
)}
<FormGroup

View File

@ -0,0 +1,24 @@
import { useCallback, useRef } from 'react';
/**
* useAutoPopulateLookup hook [... insert description]
* Param: [... insert params]
* Returns: {
* [... insert returns]
* }
*/
export default function useAutoPopulateLookup(populateLookupField) {
const isFirst = useRef(true);
return useCallback(
results => {
if (isFirst.current && results.length === 1) {
populateLookupField(results[0]);
}
isFirst.current = false;
},
[populateLookupField]
);
}