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( -