diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx
new file mode 100644
index 0000000000..451d6ea832
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx
@@ -0,0 +1,53 @@
+import React, { useState } from 'react';
+import { withRouter } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import {
+ Card,
+ CardBody,
+ CardHeader,
+ PageSection,
+ Tooltip,
+} from '@patternfly/react-core';
+import CardCloseButton from '@components/CardCloseButton';
+import JobTemplateForm from '../shared/JobTemplateForm';
+import { JobTemplatesAPI } from '@api';
+
+function JobTemplateAdd({ history, i18n }) {
+ const [error, setError] = useState('');
+
+ const handleSubmit = async values => {
+ try {
+ const data = await JobTemplatesAPI.create(values);
+ const { response } = data;
+ history.push(`/templates/${response.type}/${response.id}/details`);
+ } catch (err) {
+ setError(err);
+ }
+ };
+
+ const handleCancel = () => {
+ history.push(`/templates`);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {error ? error
: ''}
+
+
+ );
+}
+
+export default withI18n()(withRouter(JobTemplateAdd));
diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx
new file mode 100644
index 0000000000..e4b794a2f0
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import JobTemplateAdd from './JobTemplateAdd';
+
+jest.mock('@api');
+
+describe('', () => {
+ const defaultProps = {
+ description: '',
+ inventory: 0,
+ job_type: 'run',
+ name: '',
+ playbook: '',
+ project: 0,
+ };
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should render Job Template Form', () => {
+ const wrapper = mountWithContexts();
+ expect(wrapper.find('JobTemplateForm').length).toBe(1);
+ });
+
+ test('should render Job Template Form with default values', () => {
+ const wrapper = mountWithContexts();
+ expect(wrapper.find('input#template-description').props().value).toBe(
+ defaultProps.description
+ );
+ expect(wrapper.find('input#template-inventory').props().value).toBe(
+ defaultProps.inventory
+ );
+ expect(wrapper.find('AnsibleSelect[name="job_type"]').props().value).toBe(
+ defaultProps.job_type
+ );
+ expect(wrapper.find('input#template-name').props().value).toBe(
+ defaultProps.name
+ );
+ expect(wrapper.find('input#template-playbook').props().value).toBe(
+ defaultProps.playbook
+ );
+ expect(wrapper.find('input#template-project').props().value).toBe(
+ defaultProps.project
+ );
+ });
+
+ test('should navigate to templates list when cancel is clicked', () => {
+ const history = {
+ push: jest.fn(),
+ };
+ const wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
+
+ wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
+ expect(history.push).toHaveBeenCalledWith('/templates');
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/index.js b/awx/ui_next/src/screens/Template/JobTemplateAdd/index.js
new file mode 100644
index 0000000000..28af0b3a54
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/index.js
@@ -0,0 +1 @@
+export { default } from './JobTemplateAdd';
diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx
index 2e815471d6..55643616cd 100644
--- a/awx/ui_next/src/screens/Template/Templates.jsx
+++ b/awx/ui_next/src/screens/Template/Templates.jsx
@@ -8,6 +8,7 @@ import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
import { TemplateList } from './TemplateList';
import Template from './Template';
+import JobTemplateAdd from './JobTemplateAdd';
class Templates extends Component {
constructor(props) {
@@ -17,6 +18,7 @@ class Templates extends Component {
this.state = {
breadcrumbConfig: {
'/templates': i18n._(t`Templates`),
+ '/templates/job_template/add': i18n._(t`Create New Job Template`),
},
};
}
@@ -28,6 +30,7 @@ class Templates extends Component {
}
const breadcrumbConfig = {
'/templates': i18n._(t`Templates`),
+ '/templates/job_template/add': i18n._(t`Create New Job Template`),
[`/templates/${template.type}/${template.id}`]: `${template.name}`,
[`/templates/${template.type}/${template.id}/details`]: i18n._(
t`Details`
@@ -46,6 +49,10 @@ class Templates extends Component {
+ }
+ />
(
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index 536cf486e5..deac1ee9b7 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -20,11 +20,22 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
class JobTemplateForm extends Component {
static propTypes = {
- template: JobTemplate.isRequired,
+ template: JobTemplate,
handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
};
+ static defaultProps = {
+ template: {
+ name: '',
+ description: '',
+ inventory: 0,
+ job_type: 'run',
+ project: 0,
+ playbook: '',
+ },
+ };
+
render() {
const { handleCancel, handleSubmit, i18n, template } = this.props;