From 67d8c1a4b55eac732a46df0655eeff57c5342d4e Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 30 Jan 2020 10:55:50 -0800 Subject: [PATCH 01/13] delete unused/redundant component --- .../Inventory/shared/InventoryHostForm.jsx | 74 ------------------- .../shared/InventoryHostForm.test.jsx | 58 --------------- 2 files changed, 132 deletions(-) delete mode 100644 awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.jsx delete mode 100644 awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.jsx deleted file mode 100644 index f51e2abee9..0000000000 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryHostForm.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { func, shape } from 'prop-types'; -import { Formik } from 'formik'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Form } from '@patternfly/react-core'; -import FormRow from '@components/FormRow'; -import FormField from '@components/FormField'; -import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; -import { VariablesField } from '@components/CodeMirrorInput'; -import { required } from '@util/validators'; - -function InventoryHostForm({ handleSubmit, handleCancel, host, i18n }) { - return ( - - {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(); - }); -}); From b26de8b922568322686e0e276b3b0a7ff1eb43ee Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 30 Jan 2020 10:56:17 -0800 Subject: [PATCH 02/13] pass Host form API errors back into Formik for display --- .../src/screens/Host/HostAdd/HostAdd.jsx | 23 ++++++++++++++++--- .../src/screens/Host/HostEdit/HostEdit.jsx | 23 ++++++++++++++++--- .../src/screens/Host/shared/HostForm.jsx | 8 ++++++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx index f9d78a00e4..43aa5ecec8 100644 --- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx +++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx @@ -1,10 +1,14 @@ import React, { useState } from 'react'; import { useHistory, useRouteMatch } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; import { CardBody } from '@components/Card'; +import ErrorDetail from '@components/ErrorDetail'; +import AlertModal from '@components/AlertModal'; import { HostsAPI } from '@api'; import HostForm from '../shared'; -function HostAdd() { +function HostAdd({ i18n }) { const [formError, setFormError] = useState(null); const history = useHistory(); const hostsMatch = useRouteMatch('/hosts'); @@ -23,6 +27,9 @@ function HostAdd() { const { data: response } = await HostsAPI.create(values); history.push(`${url}/${response.id}/details`); } catch (error) { + if (error.response && error.response.data) { + throw error.response.data; + } setFormError(error); } }; @@ -34,9 +41,19 @@ function HostAdd() { return ( - {formError ?
error
: ''} + {formError && ( + setFormError(null)} + > + {i18n._(t`An error occurred when saving Host`)} + + + )}
); } -export default HostAdd; +export default withI18n()(HostAdd); diff --git a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx index 529ebbf425..e62fe7268c 100644 --- a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx +++ b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx @@ -1,11 +1,15 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useHistory, useRouteMatch } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; import { CardBody } from '@components/Card'; +import ErrorDetail from '@components/ErrorDetail'; +import AlertModal from '@components/AlertModal'; import { HostsAPI } from '@api'; import HostForm from '../shared'; -function HostEdit({ host }) { +function HostEdit({ host, i18n }) { const [formError, setFormError] = useState(null); const hostsMatch = useRouteMatch('/hosts/:id/edit'); const inventoriesMatch = useRouteMatch( @@ -31,6 +35,9 @@ function HostEdit({ host }) { await HostsAPI.update(host.id, values); history.push(detailsUrl); } catch (error) { + if (error.response && error.response.data) { + throw error.response.data; + } setFormError(error); } }; @@ -46,7 +53,17 @@ function HostEdit({ host }) { handleSubmit={handleSubmit} handleCancel={handleCancel} /> - {formError ?
error
: null} + {formError && ( + setFormError(null)} + > + {i18n._(t`An error occurred when saving Host`)} + + + )} ); } @@ -56,4 +73,4 @@ HostEdit.propTypes = { }; export { HostEdit as _HostEdit }; -export default HostEdit; +export default withI18n()(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..e8348dbfc1 100644 --- a/awx/ui_next/src/screens/Host/shared/HostForm.jsx +++ b/awx/ui_next/src/screens/Host/shared/HostForm.jsx @@ -30,7 +30,13 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) { inventory: host.inventory || '', variables: host.variables, }} - onSubmit={handleSubmit} + onSubmit={async (values, formik) => { + try { + await handleSubmit(values); + } catch (errors) { + formik.setErrors(errors); + } + }} > {formik => (
From b8226109a75a6420aea5209f0a4cf5472fadc247 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 31 Jan 2020 12:28:44 -0800 Subject: [PATCH 03/13] use optional chaining --- awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx | 2 +- awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx index 43aa5ecec8..b85f1130b2 100644 --- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx +++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx @@ -27,7 +27,7 @@ function HostAdd({ i18n }) { const { data: response } = await HostsAPI.create(values); history.push(`${url}/${response.id}/details`); } catch (error) { - if (error.response && error.response.data) { + if (error.response?.data) { throw error.response.data; } setFormError(error); diff --git a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx index e62fe7268c..401586d827 100644 --- a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx +++ b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx @@ -35,7 +35,7 @@ function HostEdit({ host, i18n }) { await HostsAPI.update(host.id, values); history.push(detailsUrl); } catch (error) { - if (error.response && error.response.data) { + if (error.response?.data) { throw error.response.data; } setFormError(error); From 0f9c906a22094f310ac288198d279dec5ec075dc Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 31 Jan 2020 14:10:04 -0800 Subject: [PATCH 04/13] add more robust handling of errors thrown by api --- awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx | 9 +++++++-- awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx | 3 ++- awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) 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/screens/Host/HostAdd/HostAdd.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx index b85f1130b2..b1b1d5e277 100644 --- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx +++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.jsx @@ -27,7 +27,8 @@ function HostAdd({ i18n }) { const { data: response } = await HostsAPI.create(values); history.push(`${url}/${response.id}/details`); } catch (error) { - if (error.response?.data) { + // check for field-specific errors from API + if (error.response?.data && typeof error.response.data === 'object') { throw error.response.data; } setFormError(error); diff --git a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx index 401586d827..d3fad5fb49 100644 --- a/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx +++ b/awx/ui_next/src/screens/Host/HostEdit/HostEdit.jsx @@ -35,7 +35,8 @@ function HostEdit({ host, i18n }) { await HostsAPI.update(host.id, values); history.push(detailsUrl); } catch (error) { - if (error.response?.data) { + // check for field-specific errors from API + if (error.response?.data && typeof error.response.data === 'object') { throw error.response.data; } setFormError(error); From cab25656eb7e9181e728453725fefbc974aa0823 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 3 Feb 2020 16:09:35 -0800 Subject: [PATCH 05/13] add JT form error feedback from API errors --- .../JobTemplateAdd/JobTemplateAdd.jsx | 24 ++++++++++++++-- .../JobTemplateEdit/JobTemplateEdit.jsx | 28 +++++++++++++++---- .../Template/shared/JobTemplateForm.jsx | 9 +++++- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx index 7aecba1eb2..a4511f4e10 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx @@ -1,11 +1,15 @@ import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; import { Card } from '@patternfly/react-core'; import { CardBody } from '@components/Card'; +import ErrorDetail from '@components/ErrorDetail'; +import AlertModal from '@components/AlertModal'; import JobTemplateForm from '../shared/JobTemplateForm'; import { JobTemplatesAPI } from '@api'; -function JobTemplateAdd() { +function JobTemplateAdd({ i18n }) { const [formSubmitError, setFormSubmitError] = useState(null); const history = useHistory(); @@ -31,6 +35,10 @@ function JobTemplateAdd() { ]); history.push(`/templates/${type}/${id}/details`); } catch (error) { + // check for field-specific errors from API + if (error.response?.data && typeof error.response.data === 'object') { + throw error.response.data; + } setFormSubmitError(error); } } @@ -68,9 +76,19 @@ function JobTemplateAdd() { handleSubmit={handleSubmit} /> - {formSubmitError ?
formSubmitError
: ''} + {formSubmitError && ( + setFormSubmitError(null)} + > + {i18n._(t`An error occurred when saving`)} + + + )} ); } -export default JobTemplateAdd; +export default withI18n()(JobTemplateAdd); diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 73d90a8a26..07e1085936 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -1,9 +1,13 @@ /* eslint react/no-unused-state: 0 */ import React, { Component } from 'react'; import { withRouter, Redirect } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; import { CardBody } from '@components/Card'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; +import ErrorDetail from '@components/ErrorDetail'; +import AlertModal from '@components/AlertModal'; import { JobTemplatesAPI, ProjectsAPI } from '@api'; import { JobTemplate } from '@types'; import { getAddedAndRemoved } from '@util/lists'; @@ -113,8 +117,12 @@ class JobTemplateEdit extends Component { this.submitCredentials(credentials), ]); history.push(this.detailsUrl); - } catch (formSubmitError) { - this.setState({ formSubmitError }); + } catch (error) { + // check for field-specific errors from API + if (error.response?.data && typeof error.response.data === 'object') { + throw error.response.data; + } + this.setState({ formSubmitError: error }); } } @@ -173,7 +181,7 @@ class JobTemplateEdit extends Component { } render() { - const { template } = this.props; + const { template, i18n } = this.props; const { contentError, formSubmitError, @@ -210,10 +218,20 @@ class JobTemplateEdit extends Component { handleSubmit={this.handleSubmit} relatedProjectPlaybooks={relatedProjectPlaybooks} /> - {formSubmitError ?
error
: null} + {formSubmitError && ( + this.setState({ formSubmitError: null })} + > + {i18n._(t`An error occurred when saving`)} + + + )} ); } } -export default withRouter(JobTemplateEdit); +export default withI18n()(withRouter(JobTemplateEdit)); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index c2f1ca9c4c..19973cfdd5 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -163,6 +163,7 @@ class JobTemplateForm extends Component { setFieldValue, i18n, template, + formik, } = this.props; const jobTypeOptions = [ @@ -631,7 +632,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 }; From a934e146ee22e2da554ee96136ffa6a9b2d475bd Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 4 Feb 2020 09:58:22 -0800 Subject: [PATCH 06/13] add FormSubmitError component --- .../components/FormField/FormSubmitError.jsx | 42 +++++++++++++++++++ awx/ui_next/src/components/FormField/index.js | 1 + .../JobTemplateAdd/JobTemplateAdd.jsx | 24 ++--------- .../JobTemplateEdit/JobTemplateEdit.jsx | 24 ++--------- .../Template/shared/JobTemplateForm.jsx | 14 +++++-- 5 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 awx/ui_next/src/components/FormField/FormSubmitError.jsx 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..186f17bf7c --- /dev/null +++ b/awx/ui_next/src/components/FormField/FormSubmitError.jsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react'; +import { useFormikContext } from 'formik'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import ErrorDetail from '@components/ErrorDetail'; +import AlertModal from '@components/AlertModal'; + +function FormSubmitError({ error, i18n }) { + const [formError, setFormError] = useState(null); + const { setErrors } = useFormikContext(); + + useEffect(() => { + if (!error) { + return; + } + // check for field-specific errors from API + if (error.response?.data && typeof error.response.data === 'object') { + setErrors(error.response.data); + setFormError(null); + } else { + setFormError(error); + } + }, [error, setErrors]); + + if (!formError) { + return null; + } + + return ( + setFormError(null)} + > + {i18n._(t`An error occurred when saving`)} + + + ); +} + +export default withI18n()(FormSubmitError); 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/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx index a4511f4e10..69a55550f1 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx @@ -1,15 +1,11 @@ import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; import { Card } from '@patternfly/react-core'; import { CardBody } from '@components/Card'; -import ErrorDetail from '@components/ErrorDetail'; -import AlertModal from '@components/AlertModal'; import JobTemplateForm from '../shared/JobTemplateForm'; import { JobTemplatesAPI } from '@api'; -function JobTemplateAdd({ i18n }) { +function JobTemplateAdd() { const [formSubmitError, setFormSubmitError] = useState(null); const history = useHistory(); @@ -35,10 +31,6 @@ function JobTemplateAdd({ i18n }) { ]); history.push(`/templates/${type}/${id}/details`); } catch (error) { - // check for field-specific errors from API - if (error.response?.data && typeof error.response.data === 'object') { - throw error.response.data; - } setFormSubmitError(error); } } @@ -74,21 +66,11 @@ function JobTemplateAdd({ i18n }) { - {formSubmitError && ( - setFormSubmitError(null)} - > - {i18n._(t`An error occurred when saving`)} - - - )} ); } -export default withI18n()(JobTemplateAdd); +export default JobTemplateAdd; diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 07e1085936..1e8efd94d0 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -1,13 +1,9 @@ /* eslint react/no-unused-state: 0 */ import React, { Component } from 'react'; import { withRouter, Redirect } from 'react-router-dom'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; import { CardBody } from '@components/Card'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; -import ErrorDetail from '@components/ErrorDetail'; -import AlertModal from '@components/AlertModal'; import { JobTemplatesAPI, ProjectsAPI } from '@api'; import { JobTemplate } from '@types'; import { getAddedAndRemoved } from '@util/lists'; @@ -118,10 +114,6 @@ class JobTemplateEdit extends Component { ]); history.push(this.detailsUrl); } catch (error) { - // check for field-specific errors from API - if (error.response?.data && typeof error.response.data === 'object') { - throw error.response.data; - } this.setState({ formSubmitError: error }); } } @@ -181,7 +173,7 @@ class JobTemplateEdit extends Component { } render() { - const { template, i18n } = this.props; + const { template } = this.props; const { contentError, formSubmitError, @@ -217,21 +209,11 @@ class JobTemplateEdit extends Component { handleCancel={this.handleCancel} handleSubmit={this.handleSubmit} relatedProjectPlaybooks={relatedProjectPlaybooks} + submitError={formSubmitError} /> - {formSubmitError && ( - this.setState({ formSubmitError: null })} - > - {i18n._(t`An error occurred when saving`)} - - - )} ); } } -export default withI18n()(withRouter(JobTemplateEdit)); +export default withRouter(JobTemplateEdit); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 19973cfdd5..932e610849 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,9 +167,9 @@ class JobTemplateForm extends Component { handleSubmit, handleBlur, setFieldValue, - i18n, template, - formik, + submitError, + i18n, } = this.props; const jobTypeOptions = [ @@ -202,6 +208,7 @@ class JobTemplateForm extends Component { if (contentError) { return ; } + const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div'; return ( @@ -587,6 +594,7 @@ class JobTemplateForm extends Component { + ); } From b7f3852ef9019258a68fe73a3f89a32b3514125f Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 5 Feb 2020 10:45:32 -0800 Subject: [PATCH 07/13] move FormSubmitError to inline beside form buttons; add tests --- .../FormActionGroup/FormActionGroup.jsx | 27 ++++++--- .../components/FormField/FormSubmitError.jsx | 22 ++------ .../FormField/FormSubmitError.test.jsx | 55 +++++++++++++++++++ .../Template/shared/JobTemplateForm.jsx | 7 ++- 4 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 awx/ui_next/src/components/FormField/FormSubmitError.test.jsx diff --git a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx index 16c88d8341..7ad011719b 100644 --- a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx +++ b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx @@ -1,9 +1,14 @@ 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 ErrorMessage = styled('div')` + color: var(--pf-global--danger-color--200); + font-weight: var(--pf-global--FontWeight--bold); +`; const ActionGroup = styled(PFActionGroup)` display: flex; @@ -11,19 +16,25 @@ 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; + } } `; -const FormActionGroup = ({ onSubmit, submitDisabled, onCancel, i18n }) => ( +const FormActionGroup = ({ + onSubmit, + submitDisabled, + onCancel, + errorMessage, + i18n, +}) => ( + {errorMessage ? {errorMessage} : null}