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/');
+ });
});