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 fdc1ea0cb4..8adbe3489b 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx @@ -26,6 +26,7 @@ import { const SCMSubForm = ({ autoPopulateProject, i18n }) => { const [isOpen, setIsOpen] = useState(false); + const [sourcePath, setSourcePath] = useState([]); const { setFieldValue, setFieldTouched } = useFormikContext(); const [credentialField] = useField('credential'); const [projectField, projectMeta, projectHelpers] = useField({ @@ -37,14 +38,10 @@ const SCMSubForm = ({ autoPopulateProject, i18n }) => { validate: required(i18n._(t`Select a value for this field`), i18n), }); - const { - error: sourcePathError, - request: fetchSourcePath, - result: sourcePath, - } = useRequest( + const { error: sourcePathError, request: fetchSourcePath } = useRequest( useCallback(async projectId => { const { data } = await ProjectsAPI.readInventories(projectId); - return [...data, '/ (project root)']; + setSourcePath([...data, '/ (project root)']); }, []), [] ); @@ -122,12 +119,27 @@ const SCMSubForm = ({ autoPopulateProject, i18n }) => { !sourcePathError?.message } onSelect={(event, value) => { + setIsOpen(false); + value = value.trim(); + if (!value.endsWith('/')) { + value += '/'; + } sourcePathHelpers.setValue(value); }} - isCreateable={false} + aria-label={i18n._(t`Select source path`)} + placeholder={i18n._(t`Select source path`)} + isCreatable + onCreateOption={value => { + value.trim(); + + if (!value.endsWith('/')) { + value += '/'; + } + setSourcePath([...sourcePath, value]); + }} > {sourcePath.map(path => ( - + ))} diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx index a394f76b52..9edd61b7e4 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx @@ -99,7 +99,7 @@ describe('', () => { }); wrapper.update(); expect(wrapper.find('Select#source_path').prop('selections')).toEqual( - 'bar' + 'bar/' ); await act(async () => { @@ -111,4 +111,35 @@ describe('', () => { wrapper.update(); expect(wrapper.find('Select#source_path').prop('selections')).toEqual(''); }); + + test('should be able to create custom source path', async () => { + const customInitialValues = { + credential: { id: 1, name: 'Credential' }, + custom_virtualenv: '', + overwrite: false, + overwrite_vars: false, + source_path: '/path', + source_project: { id: 1, name: 'Source project' }, + source_script: null, + source_vars: '---\n', + update_cache_timeout: 0, + update_on_launch: true, + update_on_project_update: false, + verbosity: 1, + }; + let customWrapper; + await act(async () => { + customWrapper = mountWithContexts( + + + + ); + }); + + await act(async () => { + customWrapper.find('Select').invoke('onSelect')({}, 'newPath'); + }); + customWrapper.update(); + expect(customWrapper.find('Select').prop('selections')).toBe('newPath/'); + }); });