diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index a31fd8fd7d..43440d0d22 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -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 }; diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx index 6ef8bcb979..d96617bd23 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx @@ -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( + + ); + }); + 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( + + ); + }); + 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( + + ); + }); + expect(onChange).not.toHaveBeenCalled(); + }); }); diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx index dfc4e1391f..f958d8e407 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx @@ -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 }; diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx index 999f8cdb4f..81c390a3e3 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.test.jsx @@ -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( + + ); + }); + 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(); + }); + 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( + + ); + }); + expect(onChange).not.toHaveBeenCalled(); + }); }); diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx index 3858c6a7fb..83aa0ac7ce 100644 --- a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx @@ -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: () => {}, diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.test.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.test.jsx index 09bb9e5741..04ccad63fe 100644 --- a/awx/ui_next/src/components/Lookup/ProjectLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/ProjectLookup.test.jsx @@ -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('', () => { - 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( - {}} /> - ); + mountWithContexts(); }); - 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(); + }); + expect(onChange).not.toHaveBeenCalled(); }); test('should not auto-select project when multiple available', async () => { @@ -32,13 +42,10 @@ describe('', () => { count: 2, }, }); - const autocomplete = jest.fn(); + const onChange = jest.fn(); await act(async () => { - mountWithContexts( - {}} /> - ); + mountWithContexts(); }); - await sleep(0); - expect(autocomplete).not.toHaveBeenCalled(); + expect(onChange).not.toHaveBeenCalled(); }); }); diff --git a/awx/ui_next/src/screens/Application/shared/ApplicationForm.jsx b/awx/ui_next/src/screens/Application/shared/ApplicationForm.jsx index 8d729ca073..24fd15aadc 100644 --- a/awx/ui_next/src/screens/Application/shared/ApplicationForm.jsx +++ b/awx/ui_next/src/screens/Application/shared/ApplicationForm.jsx @@ -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 ( <> organizationHelpers.setTouched()} - onChange={value => { - organizationHelpers.setValue(value); - }} + onChange={onOrganizationChange} value={organizationField.value} required + autoPopulate={!application?.id} /> { + setFieldValue('organization', value); + }, + [setFieldValue] + ); + return ( <> orgHelpers.setTouched()} - onChange={value => { - orgHelpers.setValue(value); - }} + onChange={onOrganizationChange} value={orgField.value} touched={orgMeta.touched} error={orgMeta.error} diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx index f0d779afaa..331ecd6e41 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx @@ -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 ( <> organizationHelpers.setTouched()} - onChange={value => { - organizationHelpers.setValue(value); - }} + onChange={onOrganizationChange} value={organizationField.value} touched={organizationMeta.touched} error={organizationMeta.error} required + autoPopulate={!inventory?.id} /> insightsCredentialHelpers.setValue(value)} + onChange={onCredentialChange} value={insightsCredentialField.value} /> (
- + { 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 }) => { { { - azure_rm: , + azure_rm: ( + + ), cloudforms: , ec2: , - gce: , - openstack: , - rhv: , - satellite6: , - scm: , - tower: , - vmware: , + gce: ( + + ), + openstack: ( + + ), + rhv: ( + + ), + satellite6: ( + + ), + scm: ( + + ), + tower: ( + + ), + vmware: ( + + ), }[sourceField.value] } @@ -255,6 +306,7 @@ const InventorySourceForm = ({ {submitError && } diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx index 535b364691..6be07001ed 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx index 7db4431fdd..b0a1c125d7 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx index 33447a2229..3a9ce0806d 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx @@ -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 ( <> { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx index a6c8ce5cbd..20176dd7f4 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx index 55a142e936..a1594d9111 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx index 858e209cc5..b0f846833c 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx @@ -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} /> projectHelpers.setTouched()} onChange={handleProjectUpdate} required + autoPopulate={autoPopulateProject} /> { +const SatelliteSubForm = ({ autoPopulateCredential, i18n }) => { + const { setFieldValue } = useFormikContext(); const [credentialField, credentialMeta, credentialHelpers] = useField( 'credential' ); + const handleCredentialUpdate = useCallback( + value => { + setFieldValue('credential', value); + }, + [setFieldValue] + ); + return ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx index 0dd6d7be52..917dcfcb49 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx index 08f1342cfe..54b9ff472d 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx index fda2a58ec9..85942f27a7 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx @@ -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 ( <> { helperTextInvalid={credentialMeta.error} isValid={!credentialMeta.touched || !credentialMeta.error} onBlur={() => credentialHelpers.setTouched()} - onChange={value => { - credentialHelpers.setValue(value); - }} + onChange={handleCredentialUpdate} value={credentialField.value} required + autoPopulate={autoPopulateCredential} /> diff --git a/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx index 12cd3ee215..2fa96c7ad2 100644 --- a/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/SmartInventoryForm.jsx @@ -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} /> ( - + {submitError && } { }; 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} /> ), }[formik.values.scm_type] @@ -379,6 +394,7 @@ function ProjectForm({ i18n, project, submitError, ...props }) { ', () => { ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - act(() => { + await act(async () => { wrapper.find('OrganizationLookup').invoke('onBlur')(); wrapper.find('OrganizationLookup').invoke('onChange')({ id: 1, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/InsightsSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/InsightsSubForm.jsx index 8504feed7b..5eac7c6bc1 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/InsightsSubForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/InsightsSubForm.jsx @@ -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} /> diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx index 4956c1cab1..6fc0fb1c0e 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx @@ -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 ( { - onCredentialSelection('scm', value); - credHelpers.setValue(value ? value.id : ''); - }} + onChange={onCredentialChange} /> ); } diff --git a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx index 20711f4539..3d00969c41 100644 --- a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx +++ b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx @@ -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} /> ); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx index e3e66bf8c1..2a083ffe2a 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx @@ -74,91 +74,119 @@ describe('', () => { 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( - ( - {}} 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( + ( + {}} 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( + ( + {}} 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(); }); }); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index ae68594205..8cc9c0d07d 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -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 && ( { 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} diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx index bb13418aac..00f06621c2 100644 --- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx @@ -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 ; } @@ -104,9 +110,7 @@ function WorkflowJobTemplateForm({ /> { - organizationHelpers.setValue(value || null); - }} + onChange={onOrganizationChange} value={organizationField.value} isValid={!organizationMeta.error} /> diff --git a/awx/ui_next/src/screens/User/shared/UserForm.jsx b/awx/ui_next/src/screens/User/shared/UserForm.jsx index aa5229f296..144f514c29 100644 --- a/awx/ui_next/src/screens/User/shared/UserForm.jsx +++ b/awx/ui_next/src/screens/User/shared/UserForm.jsx @@ -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 ( <> organizationHelpers.setTouched()} - onChange={value => { - organizationHelpers.setValue(value.id); - setOrganization(value); - }} + onChange={onOrganizationChange} value={organization} required + autoPopulate={!user?.id} /> )} { + if (isFirst.current && results.length === 1) { + populateLookupField(results[0]); + } + + isFirst.current = false; + }, + [populateLookupField] + ); +}