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');
});