Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Corey
d87c091eea Refactors the project form 2022-10-05 15:43:01 -04:00
9 changed files with 112 additions and 231 deletions

View File

@@ -9,7 +9,7 @@ function ProjectEdit({ project }) {
const [formSubmitError, setFormSubmitError] = useState(null); const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory(); const history = useHistory();
const handleSubmit = async (values) => { const handleSubmit = async ({ ...values }) => {
if (values.scm_type === 'manual') { if (values.scm_type === 'manual') {
values.scm_type = ''; values.scm_type = '';
} }

View File

@@ -31,6 +31,7 @@ describe('<ProjectEdit />', () => {
summary_fields: { summary_fields: {
credential: { credential: {
id: 100, id: 100,
name: 'insights',
credential_type_id: 5, credential_type_id: 5,
kind: 'insights', kind: 'insights',
}, },

View File

@@ -1,7 +1,6 @@
/* eslint no-nested-ternary: 0 */ /* eslint no-nested-ternary: 0 */
import React, { useCallback, useState, useEffect } from 'react'; import React, { useCallback, useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Formik, useField, useFormikContext } from 'formik'; import { Formik, useField, useFormikContext } from 'formik';
import { Form, FormGroup, Title } from '@patternfly/react-core'; import { Form, FormGroup, Title } from '@patternfly/react-core';
@@ -17,6 +16,7 @@ import ExecutionEnvironmentLookup from 'components/Lookup/ExecutionEnvironmentLo
import { CredentialTypesAPI, ProjectsAPI } from 'api'; import { CredentialTypesAPI, ProjectsAPI } from 'api';
import { required } from 'util/validators'; import { required } from 'util/validators';
import { FormColumnLayout, SubFormLayout } from 'components/FormLayout'; import { FormColumnLayout, SubFormLayout } from 'components/FormLayout';
import useRequest from 'hooks/useRequest';
import getProjectHelpText from './Project.helptext'; import getProjectHelpText from './Project.helptext';
import { import {
GitSubForm, GitSubForm,
@@ -26,67 +26,12 @@ import {
ManualSubForm, ManualSubForm,
} from './ProjectSubForms'; } from './ProjectSubForms';
const fetchCredentials = async (credential) => {
const [
{
data: {
results: [scmCredentialType],
},
},
{
data: {
results: [insightsCredentialType],
},
},
{
data: {
results: [cryptographyCredentialType],
},
},
] = await Promise.all([
CredentialTypesAPI.read({ kind: 'scm' }),
CredentialTypesAPI.read({ name: 'Insights' }),
CredentialTypesAPI.read({ kind: 'cryptography' }),
]);
if (!credential) {
return {
scm: { typeId: scmCredentialType.id },
insights: { typeId: insightsCredentialType.id },
cryptography: { typeId: cryptographyCredentialType.id },
};
}
const { credential_type_id } = credential;
return {
scm: {
typeId: scmCredentialType.id,
value: credential_type_id === scmCredentialType.id ? credential : null,
},
insights: {
typeId: insightsCredentialType.id,
value:
credential_type_id === insightsCredentialType.id ? credential : null,
},
cryptography: {
typeId: cryptographyCredentialType.id,
value:
credential_type_id === cryptographyCredentialType.id
? credential
: null,
},
};
};
function ProjectFormFields({ function ProjectFormFields({
project, project,
project_base_dir, project_base_dir,
project_local_paths, project_local_paths,
formik, credentialTypeIds,
setCredentials,
setSignatureValidationCredentials,
credentials,
signatureValidationCredentials,
scmTypeOptions, scmTypeOptions,
setScmSubFormState, setScmSubFormState,
scmSubFormState, scmSubFormState,
@@ -96,8 +41,7 @@ function ProjectFormFields({
scm_url: '', scm_url: '',
scm_branch: '', scm_branch: '',
scm_refspec: '', scm_refspec: '',
credential: '', credential: null,
signature_validation_credential: '',
scm_clean: false, scm_clean: false,
scm_delete_on_update: false, scm_delete_on_update: false,
scm_track_submodules: false, scm_track_submodules: false,
@@ -105,7 +49,8 @@ function ProjectFormFields({
allow_override: false, allow_override: false,
scm_update_cache_timeout: 0, scm_update_cache_timeout: 0,
}; };
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched, values, initialValues } =
useFormikContext();
const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({ const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({
name: 'scm_type', name: 'scm_type',
@@ -121,11 +66,11 @@ function ProjectFormFields({
] = useField('default_environment'); ] = useField('default_environment');
/* Save current scm subform field values to state */ /* Save current scm subform field values to state */
const saveSubFormState = (form) => { const saveSubFormState = () => {
const currentScmFormFields = { ...scmFormFields }; const currentScmFormFields = { ...scmFormFields };
Object.keys(currentScmFormFields).forEach((label) => { Object.keys(currentScmFormFields).forEach((label) => {
currentScmFormFields[label] = form.values[label]; currentScmFormFields[label] = values[label];
}); });
setScmSubFormState(currentScmFormFields); setScmSubFormState(currentScmFormFields);
@@ -137,58 +82,27 @@ function ProjectFormFields({
* If scm type is === the initial scm type value, * If scm type is === the initial scm type value,
* reset scm subform field values to scmSubFormState. * reset scm subform field values to scmSubFormState.
*/ */
const resetScmTypeFields = (value, form) => { const resetScmTypeFields = (value) => {
if (form.values.scm_type === form.initialValues.scm_type) { if (values.scm_type === initialValues.scm_type) {
saveSubFormState(formik); saveSubFormState();
} }
Object.keys(scmFormFields).forEach((label) => { Object.keys(scmFormFields).forEach((label) => {
if (value === form.initialValues.scm_type) { if (value === initialValues.scm_type) {
form.setFieldValue(label, scmSubFormState[label]); setFieldValue(label, scmSubFormState[label]);
} else { } else {
form.setFieldValue(label, scmFormFields[label]); setFieldValue(label, scmFormFields[label]);
} }
form.setFieldTouched(label, false); setFieldTouched(label, false);
}); });
}; };
const handleCredentialSelection = useCallback(
(type, value) => {
setCredentials({
...credentials,
[type]: {
...credentials[type],
value,
},
});
},
[credentials, setCredentials]
);
const handleSignatureValidationCredentialSelection = useCallback(
(type, value) => {
setSignatureValidationCredentials({
...signatureValidationCredentials,
[type]: {
...signatureValidationCredentials[type],
value,
},
});
},
[signatureValidationCredentials, setSignatureValidationCredentials]
);
const handleSignatureValidationCredentialChange = useCallback( const handleSignatureValidationCredentialChange = useCallback(
(value) => { (cred) => {
handleSignatureValidationCredentialSelection('cryptography', value); setFieldValue('signature_validation_credential', cred);
setFieldValue('signature_validation_credential', value);
setFieldTouched('signature_validation_credential', true, false); setFieldTouched('signature_validation_credential', true, false);
}, },
[ [setFieldTouched, setFieldValue]
handleSignatureValidationCredentialSelection,
setFieldValue,
setFieldTouched,
]
); );
const handleOrganizationUpdate = useCallback( const handleOrganizationUpdate = useCallback(
@@ -281,18 +195,18 @@ function ProjectFormFields({
]} ]}
onChange={(event, value) => { onChange={(event, value) => {
scmTypeHelpers.setValue(value); scmTypeHelpers.setValue(value);
resetScmTypeFields(value, formik); resetScmTypeFields(value);
}} }}
/> />
</FormGroup> </FormGroup>
<CredentialLookup <CredentialLookup
credentialTypeId={signatureValidationCredentials.cryptography.typeId} credentialTypeId={credentialTypeIds?.cryptography}
label={t`Content Signature Validation Credential`} label={t`Content Signature Validation Credential`}
onChange={handleSignatureValidationCredentialChange} onChange={handleSignatureValidationCredentialChange}
value={signatureValidationCredentials.cryptography.value} value={values.signature_validation_credential}
tooltip={projectHelpText.signatureValidation} tooltip={projectHelpText.signatureValidation}
/> />
{formik.values.scm_type !== '' && ( {values.scm_type !== '' && (
<SubFormLayout> <SubFormLayout>
<Title size="md" headingLevel="h4"> <Title size="md" headingLevel="h4">
{t`Type Details`} {t`Type Details`}
@@ -302,43 +216,39 @@ function ProjectFormFields({
{ {
manual: ( manual: (
<ManualSubForm <ManualSubForm
localPath={formik.initialValues.local_path} localPath={initialValues.local_path}
project_base_dir={project_base_dir} project_base_dir={project_base_dir}
project_local_paths={project_local_paths} project_local_paths={project_local_paths}
/> />
), ),
git: ( git: (
<GitSubForm <GitSubForm
credential={credentials.scm} credentialTypeId={credentialTypeIds.scm}
onCredentialSelection={handleCredentialSelection} scmUpdateOnLaunch={values.scm_update_on_launch}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/> />
), ),
svn: ( svn: (
<SvnSubForm <SvnSubForm
credential={credentials.scm} credentialTypeId={credentialTypeIds.scm}
onCredentialSelection={handleCredentialSelection} scmUpdateOnLaunch={values.scm_update_on_launch}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/> />
), ),
archive: ( archive: (
<ArchiveSubForm <ArchiveSubForm
credential={credentials.scm} credentialTypeId={credentialTypeIds.scm}
onCredentialSelection={handleCredentialSelection} scmUpdateOnLaunch={values.scm_update_on_launch}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/> />
), ),
insights: ( insights: (
<InsightsSubForm <InsightsSubForm
credential={credentials.insights} credentialTypeId={credentialTypeIds.insights}
onCredentialSelection={handleCredentialSelection} scmUpdateOnLaunch={values.scm_update_on_launch}
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
autoPopulateCredential={ autoPopulateCredential={
!project?.id || project?.scm_type !== 'insights' !project?.id || project?.scm_type !== 'insights'
} }
/> />
), ),
}[formik.values.scm_type] }[values.scm_type]
} }
</FormColumnLayout> </FormColumnLayout>
</SubFormLayout> </SubFormLayout>
@@ -348,16 +258,13 @@ function ProjectFormFields({
} }
function ProjectForm({ project, submitError, ...props }) { function ProjectForm({ project, submitError, ...props }) {
const { handleCancel, handleSubmit } = props; const { handleCancel, handleSubmit } = props;
const { summary_fields = {} } = project;
const { project_base_dir, project_local_paths } = useConfig(); const { project_base_dir, project_local_paths } = useConfig();
const [contentError, setContentError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [scmSubFormState, setScmSubFormState] = useState({ const [scmSubFormState, setScmSubFormState] = useState({
scm_url: '', scm_url: '',
scm_branch: '', scm_branch: '',
scm_refspec: '', scm_refspec: '',
credential: '', credential: null,
signature_validation_credential: '',
scm_clean: false, scm_clean: false,
scm_delete_on_update: false, scm_delete_on_update: false,
scm_track_submodules: false, scm_track_submodules: false,
@@ -365,27 +272,16 @@ function ProjectForm({ project, submitError, ...props }) {
allow_override: false, allow_override: false,
scm_update_cache_timeout: 0, scm_update_cache_timeout: 0,
}); });
const [scmTypeOptions, setScmTypeOptions] = useState(null);
const [credentials, setCredentials] = useState({
scm: { typeId: null, value: null },
insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null },
});
const [signatureValidationCredentials, setSignatureValidationCredentials] =
useState({
scm: { typeId: null, value: null },
insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null },
});
useEffect(() => { const {
async function fetchData() { result: { scmTypeOptions, credentialTypeIds },
try { error: contentError,
const credentialResponse = fetchCredentials(summary_fields.credential); isLoading,
const signatureValidationCredentialResponse = fetchCredentials( request: fetchData,
summary_fields.signature_validation_credential } = useRequest(
); useCallback(async () => {
const { const [
{
data: { data: {
actions: { actions: {
GET: { GET: {
@@ -393,25 +289,47 @@ function ProjectForm({ project, submitError, ...props }) {
}, },
}, },
}, },
} = await ProjectsAPI.readOptions(); },
{
setCredentials(await credentialResponse); data: {
setSignatureValidationCredentials( results: [scmCredentialType],
await signatureValidationCredentialResponse },
); },
setScmTypeOptions(choices); {
} catch (error) { data: {
setContentError(error); results: [insightsCredentialType],
} finally { },
setIsLoading(false); },
} {
data: {
results: [cryptographyCredentialType],
},
},
] = await Promise.all([
ProjectsAPI.readOptions(),
CredentialTypesAPI.read({ kind: 'scm' }),
CredentialTypesAPI.read({ name: 'Insights' }),
CredentialTypesAPI.read({ kind: 'cryptography' }),
]);
return {
scmTypeOptions: choices,
credentialTypeIds: {
scm: scmCredentialType.id,
insights: insightsCredentialType.id,
cryptography: cryptographyCredentialType.id,
},
};
}, []),
{
scmTypeOptions: [],
credentialTypeIds: { scm: '', insights: '', cryptography: '' },
isLoading: true,
} }
);
useEffect(() => {
fetchData(); fetchData();
}, [ }, [fetchData]);
summary_fields.credential,
summary_fields.signature_validation_credential,
]);
if (isLoading) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
@@ -426,11 +344,11 @@ function ProjectForm({ project, submitError, ...props }) {
initialValues={{ initialValues={{
allow_override: project.allow_override || false, allow_override: project.allow_override || false,
base_dir: project_base_dir || '', base_dir: project_base_dir || '',
credential: project.credential || '', credential: project?.summary_fields?.credential || null,
description: project.description || '', description: project.description || '',
local_path: project.local_path || '', local_path: project.local_path || '',
name: project.name || '', name: project.name || '',
organization: project.summary_fields?.organization || null, organization: project?.summary_fields?.organization || null,
scm_branch: project.scm_branch || '', scm_branch: project.scm_branch || '',
scm_clean: project.scm_clean || false, scm_clean: project.scm_clean || false,
scm_delete_on_update: project.scm_delete_on_update || false, scm_delete_on_update: project.scm_delete_on_update || false,
@@ -446,7 +364,7 @@ function ProjectForm({ project, submitError, ...props }) {
scm_update_on_launch: project.scm_update_on_launch || false, scm_update_on_launch: project.scm_update_on_launch || false,
scm_url: project.scm_url || '', scm_url: project.scm_url || '',
signature_validation_credential: signature_validation_credential:
project.signature_validation_credential || '', project?.summary_fields?.signature_validation_credential || null,
default_environment: default_environment:
project.summary_fields?.default_environment || null, project.summary_fields?.default_environment || null,
}} }}
@@ -459,13 +377,7 @@ function ProjectForm({ project, submitError, ...props }) {
project={project} 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} credentialTypeIds={credentialTypeIds}
setCredentials={setCredentials}
setSignatureValidationCredentials={
setSignatureValidationCredentials
}
credentials={credentials}
signatureValidationCredentials={signatureValidationCredentials}
scmTypeOptions={scmTypeOptions} scmTypeOptions={scmTypeOptions}
setScmSubFormState={setScmSubFormState} setScmSubFormState={setScmSubFormState}
scmSubFormState={scmSubFormState} scmSubFormState={scmSubFormState}

View File

@@ -132,8 +132,7 @@ describe('<ProjectForm />', () => {
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} /> <ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
); );
}); });
waitForElement(wrapper, 'ProjectForm', (el) => el.length === 1);
expect(wrapper.find('ProjectForm').length).toBe(1);
}); });
test('new form displays primary form fields', async () => { test('new form displays primary form fields', async () => {
@@ -149,6 +148,10 @@ describe('<ProjectForm />', () => {
expect(wrapper.find('FormGroup[label="Source Control Type"]').length).toBe( expect(wrapper.find('FormGroup[label="Source Control Type"]').length).toBe(
1 1
); );
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect(wrapper.find('FormGroup[label="Ansible Environment"]').length).toBe( expect(wrapper.find('FormGroup[label="Ansible Environment"]').length).toBe(
0 0
); );
@@ -163,7 +166,7 @@ describe('<ProjectForm />', () => {
}); });
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
await act(async () => { await act(async () => {
await wrapper.find('AnsibleSelect[id="scm_type"]').invoke('onChange')( wrapper.find('AnsibleSelect[id="scm_type"]').invoke('onChange')(
null, null,
'git' 'git'
); );
@@ -178,17 +181,9 @@ describe('<ProjectForm />', () => {
expect( expect(
wrapper.find('FormGroup[label="Source Control Refspec"]').length wrapper.find('FormGroup[label="Source Control Refspec"]').length
).toBe(1); ).toBe(1);
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect( expect(
wrapper.find('FormGroup[label="Source Control Credential"]').length wrapper.find('FormGroup[label="Source Control Credential"]').length
).toBe(1); ).toBe(1);
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1);
}); });

View File

@@ -8,19 +8,12 @@ import {
ScmTypeOptions, ScmTypeOptions,
} from './SharedFields'; } from './SharedFields';
const ArchiveSubForm = ({ const ArchiveSubForm = ({ credentialTypeId, scmUpdateOnLaunch }) => {
credential,
onCredentialSelection,
scmUpdateOnLaunch,
}) => {
const projectHelpText = getProjectHelpText(); const projectHelpText = getProjectHelpText();
return ( return (
<> <>
<UrlFormField tooltip={projectHelpText.archiveUrl} /> <UrlFormField tooltip={projectHelpText.archiveUrl} />
<ScmCredentialFormField <ScmCredentialFormField credentialTypeId={credentialTypeId} />
credential={credential}
onCredentialSelection={onCredentialSelection}
/>
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> <ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
</> </>
); );

View File

@@ -13,11 +13,7 @@ import {
} from './SharedFields'; } from './SharedFields';
import getProjectHelpStrings from '../Project.helptext'; import getProjectHelpStrings from '../Project.helptext';
const GitSubForm = ({ const GitSubForm = ({ credentialTypeId, scmUpdateOnLaunch }) => {
credential,
onCredentialSelection,
scmUpdateOnLaunch,
}) => {
const docsURL = `${getDocsBaseUrl( const docsURL = `${getDocsBaseUrl(
useConfig() useConfig()
)}/html/userguide/projects.html#manage-playbooks-using-source-control`; )}/html/userguide/projects.html#manage-playbooks-using-source-control`;
@@ -35,10 +31,7 @@ const GitSubForm = ({
tooltipMaxWidth="400px" tooltipMaxWidth="400px"
tooltip={projectHelpStrings.sourceControlRefspec(docsURL)} tooltip={projectHelpStrings.sourceControlRefspec(docsURL)}
/> />
<ScmCredentialFormField <ScmCredentialFormField credentialTypeId={credentialTypeId} />
credential={credential}
onCredentialSelection={onCredentialSelection}
/>
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> <ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
</> </>
); );

View File

@@ -1,39 +1,37 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useField, useFormikContext } from 'formik'; import { useFormikContext, useField } 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';
const InsightsSubForm = ({ const InsightsSubForm = ({
credential, credentialTypeId,
onCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
autoPopulateCredential, autoPopulateCredential,
}) => { }) => {
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [, credMeta, credHelpers] = useField('credential'); const [credField, credMeta, credHelpers] = useField('credential');
const onCredentialChange = useCallback( const onCredentialChange = useCallback(
(value) => { (value) => {
onCredentialSelection('insights', value);
setFieldValue('credential', value); setFieldValue('credential', value);
setFieldTouched('credential', true, false); setFieldTouched('credential', true, false);
}, },
[onCredentialSelection, setFieldValue, setFieldTouched] [setFieldValue, setFieldTouched]
); );
return ( return (
<> <>
<CredentialLookup <CredentialLookup
credentialTypeId={credential.typeId} credentialTypeId={credentialTypeId}
label={t`Insights Credential`} label={t`Insights Credential`}
helperTextInvalid={credMeta.error} helperTextInvalid={credMeta.error}
isValid={!credMeta.touched || !credMeta.error} isValid={!credMeta.touched || !credMeta.error}
onBlur={() => credHelpers.setTouched()} onBlur={() => credHelpers.setTouched()}
onChange={onCredentialChange} onChange={onCredentialChange}
value={credential.value} value={credField.value}
required required
autoPopulate={autoPopulateCredential} autoPopulate={autoPopulateCredential}
validate={required(t`Select a value for this field`)} validate={required(t`Select a value for this field`)}

View File

@@ -35,27 +35,23 @@ export const BranchFormField = ({ label }) => {
); );
}; };
export const ScmCredentialFormField = ({ export const ScmCredentialFormField = ({ credentialTypeId }) => {
credential, const { setFieldValue, setFieldTouched, values } = useFormikContext();
onCredentialSelection,
}) => {
const { setFieldValue, setFieldTouched } = useFormikContext();
const onCredentialChange = useCallback( const handleChange = useCallback(
(value) => { (value) => {
onCredentialSelection('scm', value);
setFieldValue('credential', value); setFieldValue('credential', value);
setFieldTouched('credential', true, false); setFieldTouched('credential', true, false);
}, },
[onCredentialSelection, setFieldValue, setFieldTouched] [setFieldValue, setFieldTouched]
); );
return ( return (
<CredentialLookup <CredentialLookup
credentialTypeId={credential.typeId} credentialTypeId={credentialTypeId}
label={t`Source Control Credential`} label={t`Source Control Credential`}
value={credential.value} value={values.credential}
onChange={onCredentialChange} onChange={handleChange}
/> />
); );
}; };

View File

@@ -10,20 +10,13 @@ import {
ScmTypeOptions, ScmTypeOptions,
} from './SharedFields'; } from './SharedFields';
const SvnSubForm = ({ const SvnSubForm = ({ credentialTypeId, scmUpdateOnLaunch }) => {
credential,
onCredentialSelection,
scmUpdateOnLaunch,
}) => {
const projectHelpStrings = getProjectHelpStrings(); const projectHelpStrings = getProjectHelpStrings();
return ( return (
<> <>
<UrlFormField tooltip={projectHelpStrings.svnSourceControlUrl} /> <UrlFormField tooltip={projectHelpStrings.svnSourceControlUrl} />
<BranchFormField label={t`Revision #`} /> <BranchFormField label={t`Revision #`} />
<ScmCredentialFormField <ScmCredentialFormField credentialTypeId={credentialTypeId} />
credential={credential}
onCredentialSelection={onCredentialSelection}
/>
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> <ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
</> </>
); );