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..5c738a942d
--- /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(null);
+
+ const handleSubmit = async values => {
+ setError(null);
+ try {
+ const { data } = await JobTemplatesAPI.create(values);
+ history.push(`/templates/${data.type}/${data.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..74dc133dc8
--- /dev/null
+++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import JobTemplateAdd from './JobTemplateAdd';
+import { JobTemplatesAPI } from '../../../api';
+
+jest.mock('@api');
+
+describe('', () => {
+ const defaultProps = {
+ description: '',
+ inventory: '',
+ job_type: 'run',
+ name: '',
+ playbook: '',
+ project: '',
+ };
+
+ 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').text()).toBe(
+ defaultProps.description
+ );
+ expect(wrapper.find('input#template-inventory').text()).toBe(
+ defaultProps.inventory
+ );
+ expect(wrapper.find('AnsibleSelect[name="job_type"]').props().value).toBe(
+ defaultProps.job_type
+ );
+ expect(
+ wrapper
+ .find('AnsibleSelect[name="job_type"]')
+ .containsAllMatchingElements([
+ ,
+ ,
+ ,
+ ])
+ ).toEqual(true);
+ expect(wrapper.find('input#template-name').text()).toBe(defaultProps.name);
+ expect(wrapper.find('input#template-playbook').text()).toBe(
+ defaultProps.playbook
+ );
+ expect(wrapper.find('input#template-project').text()).toBe(
+ defaultProps.project
+ );
+ });
+
+ test('handleSubmit should post to api', async done => {
+ const jobTemplateData = {
+ description: 'Baz',
+ inventory: 1,
+ job_type: 'run',
+ name: 'Foo',
+ playbook: 'Bar',
+ project: 2,
+ };
+ JobTemplatesAPI.create.mockResolvedValueOnce({
+ data: {
+ id: 1,
+ ...jobTemplateData,
+ },
+ });
+ const wrapper = mountWithContexts();
+ await wrapper.find('JobTemplateForm').prop('handleSubmit')(jobTemplateData);
+ expect(JobTemplatesAPI.create).toHaveBeenCalledWith(jobTemplateData);
+ done();
+ });
+
+ test('should navigate to job template detail after form submission', async done => {
+ const history = {
+ push: jest.fn(),
+ };
+ const jobTemplateData = {
+ description: 'Baz',
+ inventory: 1,
+ job_type: 'run',
+ name: 'Foo',
+ playbook: 'Bar',
+ project: 2,
+ };
+ JobTemplatesAPI.create.mockResolvedValueOnce({
+ data: {
+ id: 1,
+ type: 'job_template',
+ ...jobTemplateData,
+ },
+ });
+ const wrapper = mountWithContexts(, {
+ context: { router: { history } },
+ });
+
+ await wrapper.find('JobTemplateForm').prop('handleSubmit')(jobTemplateData);
+ expect(history.push).toHaveBeenCalledWith(
+ '/templates/job_template/1/details'
+ );
+ done();
+ });
+
+ 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/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
index 494adf0569..c488b3c8f6 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { withRouter, Redirect } from 'react-router-dom';
import { CardBody } from '@patternfly/react-core';
-import TemplateForm from '../shared/TemplateForm';
+import JobTemplateForm from '../shared/JobTemplateForm';
import { JobTemplatesAPI } from '@api';
import { JobTemplate } from '@types';
@@ -57,7 +57,7 @@ class JobTemplateEdit extends Component {
return (
- ', () => {
job_type: 'check',
};
- wrapper.find('TemplateForm').prop('handleSubmit')(updatedTemplateData);
+ wrapper.find('JobTemplateForm').prop('handleSubmit')(updatedTemplateData);
expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, updatedTemplateData);
});
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/TemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
similarity index 93%
rename from awx/ui_next/src/screens/Template/shared/TemplateForm.jsx
rename to awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index e7d91a544a..1ff5179a25 100644
--- a/awx/ui_next/src/screens/Template/shared/TemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -18,13 +18,24 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px;
`;
-class TemplateForm extends Component {
+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: '',
+ job_type: 'run',
+ project: '',
+ playbook: '',
+ },
+ };
+
render() {
const { handleCancel, handleSubmit, i18n, template } = this.props;
@@ -137,4 +148,4 @@ class TemplateForm extends Component {
}
}
-export default withI18n()(withRouter(TemplateForm));
+export default withI18n()(withRouter(JobTemplateForm));
diff --git a/awx/ui_next/src/screens/Template/shared/TemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
similarity index 94%
rename from awx/ui_next/src/screens/Template/shared/TemplateForm.test.jsx
rename to awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
index 1c6e68d22c..653cb52a41 100644
--- a/awx/ui_next/src/screens/Template/shared/TemplateForm.test.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
@@ -1,11 +1,11 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
-import TemplateForm from './TemplateForm';
+import JobTemplateForm from './JobTemplateForm';
jest.mock('@api');
-describe('', () => {
+describe('', () => {
const mockData = {
id: 1,
name: 'Foo',
@@ -23,7 +23,7 @@ describe('', () => {
test('initially renders successfully', () => {
mountWithContexts(
- ', () => {
test('should update form values on input changes', () => {
const wrapper = mountWithContexts(
- ', () => {
test('should call handleSubmit when Submit button is clicked', async () => {
const handleSubmit = jest.fn();
const wrapper = mountWithContexts(
- ', () => {
test('should call handleCancel when Cancel button is clicked', () => {
const handleCancel = jest.fn();
const wrapper = mountWithContexts(
-