Auto populate various required lookups on various forms

This commit is contained in:
mabashian
2020-09-08 14:13:53 -04:00
parent 9eb8ac620f
commit 8bee409a4a
31 changed files with 552 additions and 205 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React, { useCallback } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useField } from 'formik'; import { useField, useFormikContext } from 'formik';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import { ScmTypeOptions } from './SharedFields'; import { ScmTypeOptions } from './SharedFields';
@@ -11,13 +11,21 @@ const InsightsSubForm = ({
credential, credential,
onCredentialSelection, onCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
autoPopulateCredential,
}) => { }) => {
const credFieldArr = useField({ const { setFieldValue } = useFormikContext();
const [, credMeta, credHelpers] = useField({
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const credMeta = credFieldArr[1];
const credHelpers = credFieldArr[2]; const onCredentialChange = useCallback(
value => {
onCredentialSelection('insights', value);
setFieldValue('credential', value.id);
},
[onCredentialSelection, setFieldValue]
);
return ( return (
<> <>
@@ -27,12 +35,10 @@ const InsightsSubForm = ({
helperTextInvalid={credMeta.error} helperTextInvalid={credMeta.error}
isValid={!credMeta.touched || !credMeta.error} isValid={!credMeta.touched || !credMeta.error}
onBlur={() => credHelpers.setTouched()} onBlur={() => credHelpers.setTouched()}
onChange={value => { onChange={onCredentialChange}
onCredentialSelection('insights', value);
credHelpers.setValue(value.id);
}}
value={credential.value} value={credential.value}
required required
autoPopulate={autoPopulateCredential}
/> />
<ScmTypeOptions hideAllowOverride scmUpdateOnLaunch={scmUpdateOnLaunch} /> <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 { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useField } from 'formik'; import { useFormikContext } from 'formik';
import { FormGroup, Title } from '@patternfly/react-core'; import { FormGroup, Title } from '@patternfly/react-core';
import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup';
import FormField, { CheckboxField } from '../../../../components/FormField'; import FormField, { CheckboxField } from '../../../../components/FormField';
@@ -39,17 +39,22 @@ export const BranchFormField = withI18n()(({ i18n, label }) => (
export const ScmCredentialFormField = withI18n()( export const ScmCredentialFormField = withI18n()(
({ i18n, credential, onCredentialSelection }) => { ({ 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 ( return (
<CredentialLookup <CredentialLookup
credentialTypeId={credential.typeId} credentialTypeId={credential.typeId}
label={i18n._(t`Source Control Credential`)} label={i18n._(t`Source Control Credential`)}
value={credential.value} value={credential.value}
onChange={value => { onChange={onCredentialChange}
onCredentialSelection('scm', value);
credHelpers.setValue(value ? value.id : '');
}}
/> />
); );
} }

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import {
InputGroup, InputGroup,
Button, Button,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { useField } from 'formik'; import { useField, useFormikContext } from 'formik';
import ContentError from '../../../components/ContentError'; import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading'; import ContentLoading from '../../../components/ContentLoading';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
@@ -24,6 +24,7 @@ import {
} from '../../../api'; } from '../../../api';
function WebhookSubForm({ i18n, templateType }) { function WebhookSubForm({ i18n, templateType }) {
const { setFieldValue } = useFormikContext();
const { id } = useParams(); const { id } = useParams();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { origin } = document.location; const { origin } = document.location;
@@ -82,6 +83,14 @@ function WebhookSubForm({ i18n, templateType }) {
const changeWebhookKey = async () => { const changeWebhookKey = async () => {
await fetchWebhookKey(); await fetchWebhookKey();
}; };
const onCredentialChange = useCallback(
value => {
setFieldValue('webhook_credential', value || null);
},
[setFieldValue]
);
const isUpdateKeyDisabled = const isUpdateKeyDisabled =
pathname.endsWith('/add') || pathname.endsWith('/add') ||
webhookKeyMeta.initialValue === 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.` t`Optionally select the credential to use to send status updates back to the webhook service.`
)} )}
credentialTypeId={credTypeId} credentialTypeId={credTypeId}
onChange={value => { onChange={onCredentialChange}
webhookCredentialHelpers.setValue(value || null);
}}
isValid={!webhookCredentialMeta.error} isValid={!webhookCredentialMeta.error}
helperTextInvalid={webhookCredentialMeta.error} helperTextInvalid={webhookCredentialMeta.error}
value={webhookCredentialField.value} 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 { t } from '@lingui/macro';
import PropTypes, { shape } from 'prop-types'; import PropTypes, { shape } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { useField, withFormik } from 'formik'; import { useField, useFormikContext, withFormik } from 'formik';
import { import {
Form, Form,
FormGroup, FormGroup,
@@ -43,6 +43,7 @@ function WorkflowJobTemplateForm({
i18n, i18n,
submitError, submitError,
}) { }) {
const { setFieldValue } = useFormikContext();
const [enableWebhooks, setEnableWebhooks] = useState( const [enableWebhooks, setEnableWebhooks] = useState(
Boolean(template.webhook_service) Boolean(template.webhook_service)
); );
@@ -53,9 +54,7 @@ function WorkflowJobTemplateForm({
); );
const [labelsField, , labelsHelpers] = useField('labels'); const [labelsField, , labelsHelpers] = useField('labels');
const [limitField, limitMeta, limitHelpers] = useField('limit'); const [limitField, limitMeta, limitHelpers] = useField('limit');
const [organizationField, organizationMeta, organizationHelpers] = useField( const [organizationField, organizationMeta] = useField('organization');
'organization'
);
const [scmField, , scmHelpers] = useField('scm_branch'); const [scmField, , scmHelpers] = useField('scm_branch');
const [, webhookServiceMeta, webhookServiceHelpers] = useField( const [, webhookServiceMeta, webhookServiceHelpers] = useField(
'webhook_service' 'webhook_service'
@@ -81,6 +80,13 @@ function WorkflowJobTemplateForm({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [enableWebhooks]); }, [enableWebhooks]);
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value);
},
[setFieldValue]
);
if (hasContentError) { if (hasContentError) {
return <ContentError error={hasContentError} />; return <ContentError error={hasContentError} />;
} }
@@ -104,9 +110,7 @@ function WorkflowJobTemplateForm({
/> />
<OrganizationLookup <OrganizationLookup
helperTextInvalid={organizationMeta.error} helperTextInvalid={organizationMeta.error}
onChange={value => { onChange={onOrganizationChange}
organizationHelpers.setValue(value || null);
}}
value={organizationField.value} value={organizationField.value}
isValid={!organizationMeta.error} 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 PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Formik, useField } from 'formik'; import { Formik, useField, useFormikContext } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form, FormGroup } from '@patternfly/react-core';
import AnsibleSelect from '../../../components/AnsibleSelect'; import AnsibleSelect from '../../../components/AnsibleSelect';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
@@ -16,6 +16,7 @@ import { FormColumnLayout } from '../../../components/FormLayout';
function UserFormFields({ user, i18n }) { function UserFormFields({ user, i18n }) {
const [organization, setOrganization] = useState(null); const [organization, setOrganization] = useState(null);
const { setFieldValue } = useFormikContext();
const userTypeOptions = [ const userTypeOptions = [
{ {
@@ -38,17 +39,23 @@ function UserFormFields({ user, i18n }) {
}, },
]; ];
const organizationFieldArr = useField({ const [, organizationMeta, organizationHelpers] = useField({
name: 'organization', name: 'organization',
validate: !user.id validate: !user.id
? required(i18n._(t`Select a value for this field`), i18n) ? required(i18n._(t`Select a value for this field`), i18n)
: () => undefined, : () => undefined,
}); });
const organizationMeta = organizationFieldArr[1];
const organizationHelpers = organizationFieldArr[2];
const [userTypeField, userTypeMeta] = useField('user_type'); const [userTypeField, userTypeMeta] = useField('user_type');
const onOrganizationChange = useCallback(
value => {
setFieldValue('organization', value.id);
setOrganization(value);
},
[setFieldValue]
);
return ( return (
<> <>
<FormField <FormField
@@ -105,12 +112,10 @@ function UserFormFields({ user, i18n }) {
helperTextInvalid={organizationMeta.error} helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error} isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()} onBlur={() => organizationHelpers.setTouched()}
onChange={value => { onChange={onOrganizationChange}
organizationHelpers.setValue(value.id);
setOrganization(value);
}}
value={organization} value={organization}
required required
autoPopulate={!user?.id}
/> />
)} )}
<FormGroup <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]
);
}