diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.test.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.test.jsx index 79dfdbae42..5789183527 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.test.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.test.jsx @@ -69,6 +69,22 @@ describe('VariablesField', () => { expect(field.prop('hasErrors')).toEqual(true); expect(wrapper.find('.pf-m-error')).toHaveLength(1); }); + it('should render tooltip', () => { + const value = '---\n'; + const wrapper = mount( + + {() => ( + + )} + + ); + expect(wrapper.find('Tooltip').length).toBe(1); + }); it('should submit value through Formik', async () => { const value = '---\nfoo: bar\n'; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx index 96874360d5..40c874328c 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.jsx @@ -22,15 +22,18 @@ function WorkflowJobTemplateAdd() { setFormSubmitError(err); } }; + const submitLabels = (templateId, labels = [], organizationId) => { const associatePromises = labels.map(label => WorkflowJobTemplatesAPI.associateLabel(templateId, label, organizationId) ); return Promise.all([...associatePromises]); }; + const handleCancel = () => { history.push(`/templates`); }; + return ( @@ -38,9 +41,9 @@ function WorkflowJobTemplateAdd() { - {formSubmitError ?
formSubmitError
: ''}
); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx index 584a27cb7b..1424c689e0 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.jsx @@ -61,4 +61,27 @@ describe('', () => { }); expect(history.location.pathname).toBe('/templates'); }); + test('throwing error renders FormSubmitError component', async () => { + const error = new Error('oops'); + WorkflowJobTemplatesAPI.create.mockImplementation(() => + Promise.reject(error) + ); + await act(async () => { + await wrapper.find('WorkflowJobTemplateForm').prop('handleSubmit')({ + id: 6, + name: 'Alex', + description: 'Apollo and Athena', + inventory: { id: 1, name: 'Inventory 1' }, + organization: 2, + scm_branch: 'master', + limit: '5000', + variables: '---', + }); + }); + wrapper.update(); + expect(WorkflowJobTemplatesAPI.create).toBeCalled(); + expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual( + error + ); + }); }); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx index b8c93d96ba..6594b55def 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.jsx @@ -25,6 +25,7 @@ function WorkflowJobTemplateEdit({ setFormSubmitError(err); } }; + const submitLabels = async (labels = [], orgId) => { const { added, removed } = getAddedAndRemoved( template.summary_fields.labels.results, @@ -40,9 +41,11 @@ function WorkflowJobTemplateEdit({ setFormSubmitError(err); } } + const disassociationPromises = removed.map(label => WorkflowJobTemplatesAPI.disassociateLabel(template.id, label) ); + const associationPromises = added.map(label => { return WorkflowJobTemplatesAPI.associateLabel( template.id, @@ -61,6 +64,7 @@ function WorkflowJobTemplateEdit({ const handleCancel = () => { history.push(`/templates`); }; + if (hasTemplateLoading) { return ; } @@ -72,9 +76,9 @@ function WorkflowJobTemplateEdit({ handleCancel={handleCancel} template={template} webhook_key={webhook_key} + submitError={formSubmitError} /> - {formSubmitError ?
formSubmitError
: ''} ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx index c5bfbe7e6e..e73b29ae4e 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx @@ -94,4 +94,27 @@ describe('', () => { }); expect(history.location.pathname).toBe('/templates'); }); + test('throwing error renders FormSubmitError component', async () => { + const error = new Error('oops'); + WorkflowJobTemplatesAPI.update.mockImplementation(() => + Promise.reject(error) + ); + await act(async () => { + wrapper.find('WorkflowJobTemplateForm').prop('handleSubmit')({ + id: 6, + name: 'Alex', + description: 'Apollo and Athena', + inventory: { id: 1, name: 'Inventory 1' }, + organization: 2, + scm_branch: 'master', + limit: '5000', + variables: '---', + }); + }); + wrapper.update(); + expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalled(); + expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual( + error + ); + }); }); diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx index 00ee0beb32..68aa66983f 100644 --- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx @@ -1,5 +1,7 @@ import React, { useState, useEffect } from 'react'; import { t } from '@lingui/macro'; +import { func, shape } from 'prop-types'; + import { withI18n } from '@lingui/react'; import { Formik, Field } from 'formik'; import { @@ -11,14 +13,17 @@ import { TextInput, } from '@patternfly/react-core'; import { required } from '@util/validators'; -import PropTypes from 'prop-types'; import { SyncAltIcon } from '@patternfly/react-icons'; import { useParams } from 'react-router-dom'; import AnsibleSelect from '@components/AnsibleSelect'; import { WorkflowJobTemplatesAPI, CredentialTypesAPI } from '@api'; import FormRow from '@components/FormRow'; -import FormField, { FieldTooltip, CheckboxField } from '@components/FormField'; + +import FormField, { + FieldTooltip, + FormSubmitError, +} from '@components/FormField'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import CredentialLookup from '@components/Lookup/CredentialLookup'; import { InventoryLookup } from '@components/Lookup'; @@ -44,11 +49,11 @@ function WorkflowJobTemplateForm({ handleCancel, i18n, template = {}, - className, webhook_key, + submitError, }) { - const urlOrigin = window.location.origin; const { id } = useParams(); + const urlOrigin = window.location.origin; const [contentError, setContentError] = useState(null); const [inventory, setInventory] = useState( template?.summary_fields?.inventory || null @@ -139,9 +144,10 @@ function WorkflowJobTemplateForm({ variables: template.variables || '---', limit: template.limit || '', scmBranch: template.scm_branch || '', - allow_simultaneous: template?.allow_simultaneous || false, - webhook_url: `${urlOrigin}${template?.related?.webhook_receiver}` || '', - webhook_key: webHookKey, + allow_simultaneous: template.allow_simultaneous || false, + webhook_url: + template?.related?.webhook_receiver && + `${urlOrigin}${template?.related?.webhook_receiver}`, webhook_credential: template?.summary_fields?.webhook_credential?.id || null, webhook_service: webhookService, @@ -269,16 +275,12 @@ function WorkflowJobTemplateForm({ - + {({ field, form }) => ( + + {i18n._(t`Enable Concurrent Jobs`)}   + + + } + isChecked={field.value} + onChange={value => { + form.setFieldValue('allow_simultaneous', value); + }} + /> )} - /> + {hasWebhooks && ( <> {template.related && ( )} - {({ form }) => { - const isValid = - !form.errors.webhook_service; - return ( - - - { - setWebHookService(val); - setWebHookCredential(null); - form.setFieldValue('webhook_credential', null); - }} - /> - - ); - }} + {({ form }) => ( + + + { + setWebHookService(val); + setWebHookCredential(null); + form.setFieldValue('webhook_service', val); + form.setFieldValue('webhook_credential', null); + }} + /> + + )} {credTypeId && ( - {({ form }) => { - return ( - - { - setWebHookCredential(value); - form.setFieldValue( - 'webhook_credential', - value.id - ); - }} - value={webhookCredential} - /> - - ); - }} + {({ form }) => ( + + { + setWebHookCredential(value); + form.setFieldValue('webhook_credential', value.id); + }} + value={webhookCredential} + /> + + )} )} )} + ', () => { let wrapper; + let history; const handleSubmit = jest.fn(); const handleCancel = jest.fn(); const mockTemplate = { @@ -21,15 +31,38 @@ describe('', () => { scm_branch: 'devel', limit: '5000', variables: '---', + related: { + webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/', + }, }; beforeEach(() => { + history = createMemoryHistory({ + initialEntries: ['/templates/workflow_job_template/6/edit'], + }); act(() => { wrapper = mountWithContexts( - + ( + + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 6 } }, + }, + }, + }, + } ); }); }); @@ -63,7 +96,10 @@ describe('', () => { }); test('changing inputs should update values', async () => { const inputsToChange = [ - { element: 'wfjt-name', value: { value: 'new foo', name: 'name' } }, + { + element: 'wfjt-name', + value: { value: 'new foo', name: 'name' }, + }, { element: 'wfjt-description', value: { value: 'new bar', name: 'description' }, @@ -76,12 +112,13 @@ describe('', () => { ]; const changeInputs = async ({ element, value }) => { wrapper.find(`input#${element}`).simulate('change', { - target: value, + target: { value: `${value.value}`, name: `${value.name}` }, }); }; await act(async () => { inputsToChange.map(input => changeInputs(input)); + wrapper.find('LabelSelect').invoke('onChange')([ { name: 'new label', id: 5 }, { name: 'Label 1', id: 1 }, @@ -97,13 +134,17 @@ describe('', () => { }); }); wrapper.update(); + + expect(wrapper.find('input#wfjt-name').prop('value')).toEqual('new foo'); + const assertChanges = ({ element, value }) => { expect(wrapper.find(`input#${element}`).prop('value')).toEqual( - typeof value.value === 'string' ? `${value.value}` : value.value + `${value.value}` ); }; inputsToChange.map(input => assertChanges(input)); + expect(wrapper.find('input#wfjt-name').prop('value')).toEqual('new foo'); expect(wrapper.find('InventoryLookup').prop('value')).toEqual({ id: 3, name: 'inventory', @@ -118,16 +159,57 @@ describe('', () => { { name: 'Label 2', id: 2 }, ]); }); - test('handleSubmit is called on submit button click', async () => { - await act(async () => { - await wrapper.find('button[aria-label="Save"]').simulate('click'); - }); + test('webhooks and enable concurrent jobs functions properly', async () => { act(() => { - expect(handleSubmit).toBeCalled(); + wrapper.find('Checkbox[name="has_webhooks"]').invoke('onChange')(true); + wrapper.find('Checkbox[name="allow_simultaneous"]').invoke('onChange')( + true + ); }); + wrapper.update(); + expect( + wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly') + ).toBe(true); + expect( + wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('value') + ).toBe('sdfghjklmnbvcdsew435678iokjhgfd'); + await act(() => + wrapper + .find('FormGroup[name="webhook_key"]') + .find('Button[variant="tertiary"]') + .prop('onClick')() + ); + + expect(WorkflowJobTemplatesAPI.updateWebhookKey).toBeCalledWith('6'); + expect( + wrapper.find('TextInputBase[name="webhook_url"]').prop('value') + ).toBe('http://127.0.0.1:3001/api/v2/workflow_job_templates/57/gitlab/'); + + wrapper.update(); + + expect( + wrapper.find('Checkbox[name="has_webhooks"]').prop('isChecked') + ).toBe(true); + expect( + wrapper.find('Checkbox[name="allow_simultaneous"]').prop('isChecked') + ).toBe(true); + expect(wrapper.find('Field[name="webhook_service"]').length).toBe(1); + + act(() => wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab')); + wrapper.update(); + + expect(wrapper.find('AnsibleSelect').prop('value')).toBe('gitlab'); }); - test('handleCancel is called on cancel button click', () => { + test('handleSubmit is called on submit button click', async () => { + act(() => { + wrapper.find('Formik').prop('onSubmit')(); + }); + wrapper.update(); + sleep(0); + expect(handleSubmit).toBeCalled(); + }); + test('handleCancel is called on cancel button click', async () => { act(() => { wrapper.find('button[aria-label="Cancel"]').simulate('click'); });