diff --git a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx index 25d2c9d035..1ef3cabc99 100644 --- a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx +++ b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx @@ -53,8 +53,13 @@ class ErrorDetail extends Component { const { error } = this.props; const { response } = error; - const message = - typeof response.data === 'string' ? response.data : response.data.detail; + let message = ''; + if (response.data) { + message = + typeof response.data === 'string' + ? response.data + : response.data?.detail; + } return ( diff --git a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx index 16c88d8341..bae4fe0772 100644 --- a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx +++ b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import styled from 'styled-components'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { ActionGroup as PFActionGroup, Button } from '@patternfly/react-core'; -import styled from 'styled-components'; const ActionGroup = styled(PFActionGroup)` display: flex; @@ -11,14 +11,13 @@ const ActionGroup = styled(PFActionGroup)` --pf-c-form__group--m-action--MarginTop: 0; .pf-c-form__actions { - display: grid; - gap: 24px; - grid-template-columns: auto auto; - margin: 0; - & > button { margin: 0; } + + & > :not(:first-child) { + margin-left: 24px; + } } `; diff --git a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.test.jsx b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.test.jsx index 9290e1aa72..d81d9e9e59 100644 --- a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.test.jsx +++ b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.test.jsx @@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers'; import FormActionGroup from './FormActionGroup'; describe('FormActionGroup', () => { - test('renders the expected content', () => { + test('should render the expected content', () => { const wrapper = mountWithContexts( {}} onCancel={() => {}} /> ); diff --git a/awx/ui_next/src/components/FormField/FormSubmitError.jsx b/awx/ui_next/src/components/FormField/FormSubmitError.jsx new file mode 100644 index 0000000000..6dd5db1c32 --- /dev/null +++ b/awx/ui_next/src/components/FormField/FormSubmitError.jsx @@ -0,0 +1,35 @@ +import React, { useState, useEffect } from 'react'; +import { useFormikContext } from 'formik'; +import { Alert } from '@patternfly/react-core'; + +function FormSubmitError({ error }) { + const [errorMessage, setErrorMessage] = useState(null); + const { setErrors } = useFormikContext(); + + useEffect(() => { + if (!error) { + return; + } + if (error?.response?.data && typeof error.response.data === 'object') { + const errorMessages = error.response.data; + setErrors(errorMessages); + if (errorMessages.__all__) { + setErrorMessage(errorMessages.__all__); + } else { + setErrorMessage(null); + } + } else { + /* eslint-disable-next-line no-console */ + console.error(error); + setErrorMessage(error.message); + } + }, [error, setErrors]); + + if (!errorMessage) { + return null; + } + + return ; +} + +export default FormSubmitError; diff --git a/awx/ui_next/src/components/FormField/FormSubmitError.test.jsx b/awx/ui_next/src/components/FormField/FormSubmitError.test.jsx new file mode 100644 index 0000000000..a52211d667 --- /dev/null +++ b/awx/ui_next/src/components/FormField/FormSubmitError.test.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { Formik } from 'formik'; +import FormSubmitError from './FormSubmitError'; + +describe('', () => { + test('should render null when no error present', () => { + const wrapper = mountWithContexts( + {() => } + ); + expect(wrapper.find('FormSubmitError').text()).toEqual(''); + }); + + test('should pass field errors to Formik', () => { + const error = { + response: { + data: { + name: 'invalid', + }, + }, + }; + const wrapper = mountWithContexts( + + {({ errors }) => ( +
+

{errors.name}

+ +
+ )} +
+ ); + expect(wrapper.find('p').text()).toEqual('invalid'); + }); + + test('should display error message if field errors not provided', async () => { + const realConsole = global.console; + global.console = { + error: jest.fn(), + }; + const error = { + message: 'There was an error', + }; + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + {() => } + ); + }); + wrapper.update(); + expect(wrapper.find('Alert').prop('title')).toEqual('There was an error'); + expect(global.console.error).toHaveBeenCalledWith(error); + global.console = realConsole; + }); +}); diff --git a/awx/ui_next/src/components/FormField/index.js b/awx/ui_next/src/components/FormField/index.js index 2b23e65900..563f8519eb 100644 --- a/awx/ui_next/src/components/FormField/index.js +++ b/awx/ui_next/src/components/FormField/index.js @@ -2,3 +2,4 @@ export { default } from './FormField'; export { default as CheckboxField } from './CheckboxField'; export { default as FieldTooltip } from './FieldTooltip'; export { default as PasswordField } from './PasswordField'; +export { default as FormSubmitError } from './FormSubmitError'; diff --git a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx index f9d78a00e4..422cabb721 100644 --- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx +++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx @@ -33,8 +33,11 @@ function HostAdd() { return ( - - {formError ?
error
: ''} +
); } diff --git a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx index 2a0e419675..ebb302fc42 100644 --- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx +++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx @@ -30,6 +30,12 @@ describe('', () => { }); test('handleSubmit should post to api', async () => { + HostsAPI.create.mockResolvedValueOnce({ + data: { + ...hostData, + id: 5, + }, + }); await act(async () => { wrapper.find('HostForm').prop('handleSubmit')(hostData); }); diff --git a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx index 529ebbf425..d2ef0252e9 100644 --- a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx +++ b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx @@ -45,8 +45,8 @@ function HostEdit({ host }) { host={host} handleSubmit={handleSubmit} handleCancel={handleCancel} + submitError={formError} /> - {formError ?
error
: null} ); } @@ -55,5 +55,4 @@ HostEdit.propTypes = { host: PropTypes.shape().isRequired, }; -export { HostEdit as _HostEdit }; export default HostEdit; diff --git a/awx/ui_next/src/screens/Host/shared/HostForm.jsx b/awx/ui_next/src/screens/Host/shared/HostForm.jsx index 4c2a1c38b4..0e1919be58 100644 --- a/awx/ui_next/src/screens/Host/shared/HostForm.jsx +++ b/awx/ui_next/src/screens/Host/shared/HostForm.jsx @@ -9,13 +9,13 @@ import { t } from '@lingui/macro'; import { Form } from '@patternfly/react-core'; import FormRow from '@components/FormRow'; -import FormField from '@components/FormField'; +import FormField, { FormSubmitError } from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import { VariablesField } from '@components/CodeMirrorInput'; import { required } from '@util/validators'; import { InventoryLookup } from '@components/Lookup'; -function HostForm({ handleSubmit, handleCancel, host, i18n }) { +function HostForm({ handleSubmit, handleCancel, host, submitError, i18n }) { const [inventory, setInventory] = useState( host ? host.summary_fields.inventory : '' ); @@ -85,6 +85,7 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) { label={i18n._(t`Variables`)} /> + - - - - - - - ); - } if (isLoading) { return ; } @@ -91,6 +79,7 @@ function InventoryAdd() { onCancel={handleCancel} onSubmit={handleSubmit} credentialTypeId={credentialTypeId} + submitError={error} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx index f0b82ae943..cbc1b2126d 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx @@ -5,7 +5,6 @@ import { object } from 'prop-types'; import { CardBody } from '@components/Card'; import { InventoriesAPI, CredentialTypesAPI } from '@api'; import ContentLoading from '@components/ContentLoading'; -import ContentError from '@components/ContentError'; import InventoryForm from '../shared/InventoryForm'; import { getAddedAndRemoved } from '../../../util/lists'; @@ -105,10 +104,6 @@ function InventoryEdit({ inventory }) { return ; } - if (error) { - return ; - } - return ( ); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx index 79dd3023bf..6a27f1bd70 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx @@ -6,7 +6,7 @@ import { func, number, shape } from 'prop-types'; import { VariablesField } from '@components/CodeMirrorInput'; import { Form } from '@patternfly/react-core'; -import FormField from '@components/FormField'; +import FormField, { FormSubmitError } from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormRow from '@components/FormRow'; import { required } from '@util/validators'; @@ -21,6 +21,7 @@ function InventoryForm({ onSubmit, instanceGroups, credentialTypeId, + submitError, }) { const initialValues = { name: inventory.name || '', @@ -129,6 +130,7 @@ function InventoryForm({ /> + - {formik => ( -
- - - - - - - - { - formik.handleSubmit(); - }} - /> - - )} - - ); -} - -InventoryHostForm.propTypes = { - handleSubmit: func.isRequired, - handleCancel: func.isRequired, - host: shape({}), -}; - -InventoryHostForm.defaultProps = { - host: { - name: '', - description: '', - variables: '---\n', - }, -}; - -export default withI18n()(InventoryHostForm); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.test.jsx deleted file mode 100644 index 72a1d3b08e..0000000000 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.test.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mountWithContexts } from '@testUtils/enzymeHelpers'; -import { sleep } from '@testUtils/testUtils'; -import InventoryHostForm from './InventoryHostForm'; - -jest.mock('@api'); - -describe('', () => { - let wrapper; - - const handleSubmit = jest.fn(); - const handleCancel = jest.fn(); - - const mockHostData = { - name: 'foo', - description: 'bar', - inventory: 1, - variables: '---\nfoo: bar', - }; - - beforeEach(async () => { - await act(async () => { - wrapper = mountWithContexts( - - ); - }); - }); - - afterEach(() => { - wrapper.unmount(); - }); - - test('should display form fields', () => { - expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); - expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); - expect(wrapper.find('VariablesField').length).toBe(1); - }); - - test('should call handleSubmit when Submit button is clicked', async () => { - expect(handleSubmit).not.toHaveBeenCalled(); - await act(async () => { - wrapper.find('button[aria-label="Save"]').simulate('click'); - }); - expect(handleSubmit).toHaveBeenCalled(); - }); - - test('should call handleCancel when Cancel button is clicked', async () => { - expect(handleCancel).not.toHaveBeenCalled(); - wrapper.find('button[aria-label="Cancel"]').simulate('click'); - await sleep(1); - expect(handleCancel).toHaveBeenCalled(); - }); -}); diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx index 3d501b2d71..90ffdfccd2 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx @@ -40,10 +40,10 @@ function OrganizationAdd() { onSubmit={handleSubmit} onCancel={handleCancel} me={me || {}} + submitError={formError} /> )} - {formError ?
error
: ''} diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx index 0e499e8cda..4618508afc 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx @@ -14,6 +14,7 @@ describe('', () => { description: 'new description', custom_virtualenv: 'Buzz', }; + OrganizationsAPI.create.mockResolvedValueOnce({ data: {} }); await act(async () => { const wrapper = mountWithContexts(); wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []); diff --git a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx index d2aec4b182..04c13c8848 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx @@ -48,10 +48,10 @@ function OrganizationEdit({ organization }) { onSubmit={handleSubmit} onCancel={handleCancel} me={me || {}} + submitError={formError} /> )} - {formError ?
error
: null} ); } diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index 2d1cc69347..abbf4eedc1 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -13,13 +13,20 @@ import AnsibleSelect from '@components/AnsibleSelect'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; import FormRow from '@components/FormRow'; -import FormField from '@components/FormField'; +import FormField, { FormSubmitError } from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import { InstanceGroupsLookup } from '@components/Lookup/'; import { getAddedAndRemoved } from '@util/lists'; import { required, minMaxValue } from '@util/validators'; -function OrganizationForm({ organization, i18n, me, onCancel, onSubmit }) { +function OrganizationForm({ + organization, + i18n, + me, + onCancel, + onSubmit, + submitError, +}) { const defaultVenv = { label: i18n._(t`Use Default Ansible Environment`), value: '/venv/ansible/', @@ -161,6 +168,7 @@ function OrganizationForm({ organization, i18n, me, onCancel, onSubmit }) { t`Select the Instance Groups for this Organization to run on.` )} /> + - {formSubmitError ? ( -
formSubmitError
- ) : ( - '' - )} ); diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx index e24249a739..b4048ed55c 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -106,7 +106,8 @@ describe('', () => { project_local_paths: ['foobar', 'qux'], project_base_dir: 'dir/foo/bar', }; - ProjectsAPI.create.mockImplementation(() => Promise.reject(new Error())); + const error = new Error('oops'); + ProjectsAPI.create.mockImplementation(() => Promise.reject(error)); await act(async () => { wrapper = mountWithContexts(, { context: { config }, @@ -121,7 +122,7 @@ describe('', () => { }); wrapper.update(); expect(ProjectsAPI.create).toHaveBeenCalledTimes(1); - expect(wrapper.find('ProjectAdd .formSubmitError').length).toBe(1); + expect(wrapper.find('ProjectForm').prop('submitError')).toEqual(error); }); test('CardBody cancel button should navigate to projects list', async () => { diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx index 6f44ec8ab0..64ebf65c05 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.jsx @@ -42,13 +42,9 @@ function ProjectEdit({ project }) { project={project} handleCancel={handleCancel} handleSubmit={handleSubmit} + submitError={formSubmitError} /> - {formSubmitError ? ( -
formSubmitError
- ) : ( - '' - )} ); } diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx index 6f8ca42945..ceb0e2c5b2 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx @@ -120,7 +120,10 @@ describe('', () => { project_local_paths: [], project_base_dir: 'foo/bar', }; - ProjectsAPI.update.mockImplementation(() => Promise.reject(new Error())); + const error = new Error('oops'); + const realConsoleError = global.console.error; + global.console.error = jest.fn(); + ProjectsAPI.update.mockImplementation(() => Promise.reject(error)); await act(async () => { wrapper = mountWithContexts( , @@ -135,7 +138,8 @@ describe('', () => { }); wrapper.update(); expect(ProjectsAPI.update).toHaveBeenCalledTimes(1); - expect(wrapper.find('ProjectEdit .formSubmitError').length).toBe(1); + expect(wrapper.find('ProjectForm').prop('submitError')).toEqual(error); + global.console.error = realConsoleError; }); test('CardBody cancel button should navigate to project details', async () => { diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index ae72bd0896..768624dc7c 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -10,7 +10,10 @@ import AnsibleSelect from '@components/AnsibleSelect'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; -import FormField, { FieldTooltip } from '@components/FormField'; +import FormField, { + FieldTooltip, + FormSubmitError, +} from '@components/FormField'; import FormRow from '@components/FormRow'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { CredentialTypesAPI, ProjectsAPI } from '@api'; @@ -70,7 +73,7 @@ const fetchCredentials = async credential => { }; }; -function ProjectForm({ project, ...props }) { +function ProjectForm({ project, submitError, ...props }) { const { i18n, handleCancel, handleSubmit } = props; const { summary_fields = {} } = project; const [contentError, setContentError] = useState(null); @@ -385,6 +388,7 @@ function ProjectForm({ project, ...props }) { }
+ )} - {error ?
error
: ''} diff --git a/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx b/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx index 07aa2382fc..7d1eddeabf 100644 --- a/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx +++ b/awx/ui_next/src/screens/Team/TeamAdd/TeamAdd.test.jsx @@ -9,6 +9,7 @@ jest.mock('@api'); describe('', () => { test('handleSubmit should post to api', async () => { + TeamsAPI.create.mockResolvedValueOnce({ data: {} }); const wrapper = mountWithContexts(); const updatedTeamData = { name: 'new name', diff --git a/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx b/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx index e11a014018..19a6ffe03a 100644 --- a/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx +++ b/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx @@ -34,10 +34,10 @@ function TeamEdit({ team }) { handleSubmit={handleSubmit} handleCancel={handleCancel} me={me || {}} + submitError={error} /> )} - {error ?
error
: null} ); } diff --git a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx index e3414950f2..040d6d4dca 100644 --- a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx +++ b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx @@ -5,13 +5,13 @@ import { t } from '@lingui/macro'; import { Formik, Field } from 'formik'; import { Form } from '@patternfly/react-core'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; -import FormField from '@components/FormField'; +import FormField, { FormSubmitError } from '@components/FormField'; import FormRow from '@components/FormRow'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { required } from '@util/validators'; function TeamForm(props) { - const { team, handleCancel, handleSubmit, i18n } = props; + const { team, handleCancel, handleSubmit, submitError, i18n } = props; const [organization, setOrganization] = useState( team.summary_fields ? team.summary_fields.organization : null ); @@ -70,6 +70,7 @@ function TeamForm(props) { )} + - {formSubmitError ?
formSubmitError
: ''} ); } diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 73d90a8a26..1e8efd94d0 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -113,8 +113,8 @@ class JobTemplateEdit extends Component { this.submitCredentials(credentials), ]); history.push(this.detailsUrl); - } catch (formSubmitError) { - this.setState({ formSubmitError }); + } catch (error) { + this.setState({ formSubmitError: error }); } } @@ -209,8 +209,8 @@ class JobTemplateEdit extends Component { handleCancel={this.handleCancel} handleSubmit={this.handleSubmit} relatedProjectPlaybooks={relatedProjectPlaybooks} + submitError={formSubmitError} /> - {formSubmitError ?
error
: null} ); } diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index c2f1ca9c4c..326ae5f389 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -16,7 +16,11 @@ import ContentLoading from '@components/ContentLoading'; import AnsibleSelect from '@components/AnsibleSelect'; import { TagMultiSelect } from '@components/MultiSelect'; import FormActionGroup from '@components/FormActionGroup'; -import FormField, { CheckboxField, FieldTooltip } from '@components/FormField'; +import FormField, { + CheckboxField, + FieldTooltip, + FormSubmitError, +} from '@components/FormField'; import FormRow from '@components/FormRow'; import CollapsibleSection from '@components/CollapsibleSection'; import { required } from '@util/validators'; @@ -48,6 +52,7 @@ class JobTemplateForm extends Component { template: JobTemplate, handleCancel: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, + submitError: PropTypes.shape({}), }; static defaultProps = { @@ -66,6 +71,7 @@ class JobTemplateForm extends Component { }, isNew: true, }, + submitError: null, }; constructor(props) { @@ -161,8 +167,9 @@ class JobTemplateForm extends Component { handleSubmit, handleBlur, setFieldValue, - i18n, template, + submitError, + i18n, } = this.props; const jobTypeOptions = [ @@ -201,6 +208,7 @@ class JobTemplateForm extends Component { if (contentError) { return ; } + const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div'; return (
@@ -585,6 +593,7 @@ class JobTemplateForm extends Component { + ); @@ -631,7 +640,13 @@ const FormikApp = withFormik({ credentials: summary_fields.credentials || [], }; }, - handleSubmit: (values, { props }) => props.handleSubmit(values), + handleSubmit: async (values, { props, setErrors }) => { + try { + await props.handleSubmit(values); + } catch (errors) { + setErrors(errors); + } + }, })(JobTemplateForm); export { JobTemplateForm as _JobTemplateForm }; diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx index eb1bf9fff7..0694627815 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx @@ -35,13 +35,12 @@ function UserAdd() { - + - {formSubmitError ? ( -
formSubmitError
- ) : ( - '' - )}
); diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx index 0ea4d56e19..1537855d0e 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx @@ -13,6 +13,7 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts(); }); + UsersAPI.create.mockResolvedValueOnce({ data: {} }); const updatedUserData = { username: 'sysadmin', email: 'sysadmin@ansible.com', diff --git a/awx/ui_next/src/screens/User/UserEdit/UserEdit.jsx b/awx/ui_next/src/screens/User/UserEdit/UserEdit.jsx index 01f3788647..5cd6f0452b 100644 --- a/awx/ui_next/src/screens/User/UserEdit/UserEdit.jsx +++ b/awx/ui_next/src/screens/User/UserEdit/UserEdit.jsx @@ -27,8 +27,8 @@ function UserEdit({ user, history }) { user={user} handleCancel={handleCancel} handleSubmit={handleSubmit} + submitError={formSubmitError} /> - {formSubmitError ?
error
: null} ); } diff --git a/awx/ui_next/src/screens/User/shared/UserForm.jsx b/awx/ui_next/src/screens/User/shared/UserForm.jsx index 6620049c45..9c1f2d7138 100644 --- a/awx/ui_next/src/screens/User/shared/UserForm.jsx +++ b/awx/ui_next/src/screens/User/shared/UserForm.jsx @@ -6,13 +6,15 @@ import { Formik, Field } from 'formik'; import { Form, FormGroup } from '@patternfly/react-core'; import AnsibleSelect from '@components/AnsibleSelect'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; -import FormField, { PasswordField } from '@components/FormField'; +import FormField, { + PasswordField, + FormSubmitError, +} from '@components/FormField'; import FormRow from '@components/FormRow'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { required, requiredEmail } from '@util/validators'; -function UserForm(props) { - const { user, handleCancel, handleSubmit, i18n } = props; +function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) { const [organization, setOrganization] = useState(null); const userTypeOptions = [ @@ -183,6 +185,7 @@ function UserForm(props) { }} +