diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js
index d295502309..58f628e75d 100644
--- a/awx/ui_next/src/api/index.js
+++ b/awx/ui_next/src/api/index.js
@@ -6,6 +6,7 @@ import Jobs from './models/Jobs';
import Labels from './models/Labels';
import Me from './models/Me';
import Organizations from './models/Organizations';
+import Projects from './models/Projects';
import Root from './models/Root';
import Teams from './models/Teams';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
@@ -21,6 +22,7 @@ const JobsAPI = new Jobs();
const LabelsAPI = new Labels();
const MeAPI = new Me();
const OrganizationsAPI = new Organizations();
+const ProjectsAPI = new Projects();
const RootAPI = new Root();
const TeamsAPI = new Teams();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
@@ -37,6 +39,7 @@ export {
LabelsAPI,
MeAPI,
OrganizationsAPI,
+ ProjectsAPI,
RootAPI,
TeamsAPI,
UnifiedJobTemplatesAPI,
diff --git a/awx/ui_next/src/api/models/Projects.js b/awx/ui_next/src/api/models/Projects.js
new file mode 100644
index 0000000000..a5a7d90042
--- /dev/null
+++ b/awx/ui_next/src/api/models/Projects.js
@@ -0,0 +1,10 @@
+import Base from '../Base';
+
+class Projects extends Base {
+ constructor(http) {
+ super(http);
+ this.baseUrl = '/api/v2/projects/';
+ }
+}
+
+export default Projects;
diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx
index 8869abd5f8..32910b10ef 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx
@@ -57,9 +57,7 @@ describe('', () => {
expect(wrapper.find('input#template-playbook').text()).toBe(
defaultProps.playbook
);
- expect(wrapper.find('input#template-project').text()).toBe(
- defaultProps.project
- );
+ expect(wrapper.find('ProjectLookup').prop('value')).toBe(null);
done();
});
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index c12e50fd5a..bc5e593b19 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -17,6 +17,7 @@ import { required } from '@util/validators';
import styled from 'styled-components';
import { JobTemplate } from '@types';
import InventoriesLookup from './InventoriesLookup';
+import ProjectLookup from './ProjectLookup';
import { LabelsAPI } from '@api';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
@@ -56,6 +57,7 @@ class JobTemplateForm extends Component {
loadedLabels: [],
newLabels: [],
removedLabels: [],
+ project: props.template.summary_fields.project,
inventory: props.template.summary_fields.inventory,
};
this.handleNewLabel = this.handleNewLabel.bind(this);
@@ -153,6 +155,7 @@ class JobTemplateForm extends Component {
contentError,
hasContentLoading,
inventory,
+ project,
newLabels,
removedLabels,
} = this.state;
@@ -258,15 +261,21 @@ class JobTemplateForm extends Component {
/>
)}
/>
- (
+ {
+ form.setFieldValue('project', value.id);
+ this.setState({ project: value });
+ }}
+ required
+ />
+ )}
/>
', () => {
name: 'foo',
organization_id: 1,
},
+ project: {
+ id: 3,
+ name: 'qux',
+ },
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
},
};
@@ -44,9 +48,13 @@ describe('', () => {
/>
);
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
- const component = wrapper.find('ChipGroup');
expect(LabelsAPI.read).toHaveBeenCalled();
- expect(component.find('span#pf-random-id-1').text()).toEqual('Sushi');
+ expect(
+ wrapper
+ .find('FormGroup[fieldId="template-labels"] MultiSelect Chip')
+ .first()
+ .text()
+ ).toEqual('Sushi');
done();
});
@@ -77,8 +85,9 @@ describe('', () => {
name: 'inventory',
});
expect(form.state('values').inventory).toEqual(3);
- wrapper.find('input#template-project').simulate('change', {
- target: { value: 4, name: 'project' },
+ wrapper.find('ProjectLookup').prop('onChange')({
+ id: 4,
+ name: 'project',
});
expect(form.state('values').project).toEqual(4);
wrapper.find('input#template-playbook').simulate('change', {
diff --git a/awx/ui_next/src/screens/Template/shared/ProjectLookup.jsx b/awx/ui_next/src/screens/Template/shared/ProjectLookup.jsx
new file mode 100644
index 0000000000..edded07d57
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/shared/ProjectLookup.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { string, func, bool } from 'prop-types';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { FormGroup, Tooltip } from '@patternfly/react-core';
+import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
+import { ProjectsAPI } from '@api';
+import { Project } from '@types';
+import Lookup from '@components/Lookup';
+import styled from 'styled-components';
+
+const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
+ margin-left: 10px;
+`;
+
+const loadProjects = async params => ProjectsAPI.read(params);
+
+class ProjectLookup extends React.Component {
+ render() {
+ const { value, tooltip, onChange, required, i18n } = this.props;
+
+ return (
+
+ {tooltip && (
+
+
+
+ )}
+
+
+ );
+ }
+}
+
+ProjectLookup.propTypes = {
+ value: Project,
+ tooltip: string,
+ onChange: func.isRequired,
+ required: bool,
+};
+
+ProjectLookup.defaultProps = {
+ value: null,
+ tooltip: '',
+ required: false,
+};
+
+export default withI18n()(ProjectLookup);