From b06421b8709409a4dbd97ea41d5a9ca3147b20b9 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 27 Jun 2019 11:46:52 -0400 Subject: [PATCH 1/3] Rename TemplateForm to JobTemplateForm --- .../Template/JobTemplateEdit/JobTemplateEdit.jsx | 4 ++-- .../JobTemplateEdit/JobTemplateEdit.test.jsx | 2 +- .../shared/{TemplateForm.jsx => JobTemplateForm.jsx} | 4 ++-- ...emplateForm.test.jsx => JobTemplateForm.test.jsx} | 12 ++++++------ awx/ui_next/src/screens/Template/shared/index.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) rename awx/ui_next/src/screens/Template/shared/{TemplateForm.jsx => JobTemplateForm.jsx} (97%) rename awx/ui_next/src/screens/Template/shared/{TemplateForm.test.jsx => JobTemplateForm.test.jsx} (94%) 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/shared/TemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx similarity index 97% rename from awx/ui_next/src/screens/Template/shared/TemplateForm.jsx rename to awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index e7d91a544a..536cf486e5 100644 --- a/awx/ui_next/src/screens/Template/shared/TemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -18,7 +18,7 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)` margin-left: 10px; `; -class TemplateForm extends Component { +class JobTemplateForm extends Component { static propTypes = { template: JobTemplate.isRequired, handleCancel: PropTypes.func.isRequired, @@ -137,4 +137,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( - Date: Mon, 1 Jul 2019 16:22:31 -0400 Subject: [PATCH 2/3] Add Job Template Add form skeleton and test --- .../JobTemplateAdd/JobTemplateAdd.jsx | 53 +++++++++++++++++ .../JobTemplateAdd/JobTemplateAdd.test.jsx | 59 +++++++++++++++++++ .../screens/Template/JobTemplateAdd/index.js | 1 + .../src/screens/Template/Templates.jsx | 7 +++ .../Template/shared/JobTemplateForm.jsx | 13 +++- 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx create mode 100644 awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx create mode 100644 awx/ui_next/src/screens/Template/JobTemplateAdd/index.js 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; From fb0c82598f0e007d68fa3d2acfe9fb854d371ecd Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 3 Jul 2019 16:54:40 -0400 Subject: [PATCH 3/3] Address PR feedback --- .../JobTemplateAdd/JobTemplateAdd.jsx | 8 +- .../JobTemplateAdd/JobTemplateAdd.test.jsx | 77 ++++++++++++++++--- .../Template/shared/JobTemplateForm.jsx | 4 +- awx/ui_next/src/types.js | 14 +++- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx index 451d6ea832..5c738a942d 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx @@ -14,13 +14,13 @@ import JobTemplateForm from '../shared/JobTemplateForm'; import { JobTemplatesAPI } from '@api'; function JobTemplateAdd({ history, i18n }) { - const [error, setError] = useState(''); + const [error, setError] = useState(null); const handleSubmit = async values => { + setError(null); try { - const data = await JobTemplatesAPI.create(values); - const { response } = data; - history.push(`/templates/${response.type}/${response.id}/details`); + const { data } = await JobTemplatesAPI.create(values); + history.push(`/templates/${data.type}/${data.id}/details`); } catch (err) { setError(err); } 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 e4b794a2f0..74dc133dc8 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx @@ -1,17 +1,18 @@ 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: 0, + inventory: '', job_type: 'run', name: '', playbook: '', - project: 0, + project: '', }; afterEach(() => { @@ -25,26 +26,84 @@ describe('', () => { test('should render Job Template Form with default values', () => { const wrapper = mountWithContexts(); - expect(wrapper.find('input#template-description').props().value).toBe( + expect(wrapper.find('input#template-description').text()).toBe( defaultProps.description ); - expect(wrapper.find('input#template-inventory').props().value).toBe( + 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('input#template-name').props().value).toBe( - defaultProps.name - ); - expect(wrapper.find('input#template-playbook').props().value).toBe( + 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').props().value).toBe( + 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(), diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index deac1ee9b7..1ff5179a25 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -29,9 +29,9 @@ class JobTemplateForm extends Component { template: { name: '', description: '', - inventory: 0, + inventory: '', job_type: 'run', - project: 0, + project: '', playbook: '', }, }; diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 0681fe3c48..ada500bc0c 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -1,4 +1,12 @@ -import { shape, arrayOf, number, string, bool, oneOf } from 'prop-types'; +import { + shape, + arrayOf, + number, + string, + bool, + oneOf, + oneOfType, +} from 'prop-types'; export const Role = shape({ descendent_roles: arrayOf(string), @@ -57,8 +65,8 @@ export const QSConfig = shape({ export const JobTemplate = shape({ name: string.isRequired, description: string, - inventory: number.isRequired, + inventory: oneOfType([number, string]).isRequired, job_type: oneOf(['run', 'check']), playbook: string.isRequired, - project: number.isRequired, + project: oneOfType([number, string]).isRequired, });