Merge pull request #5842 from keithjgrant/4240-form-error-handling

Form error handling

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-02-11 01:16:47 +00:00 committed by GitHub
commit 5ed623d682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 206 additions and 207 deletions

View File

@ -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 (
<Fragment>

View File

@ -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;
}
}
`;

View File

@ -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(
<FormActionGroup onSubmit={() => {}} onCancel={() => {}} />
);

View File

@ -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 <Alert variant="danger" isInline title={errorMessage} />;
}
export default FormSubmitError;

View File

@ -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('<FormSubmitError>', () => {
test('should render null when no error present', () => {
const wrapper = mountWithContexts(
<Formik>{() => <FormSubmitError error={null} />}</Formik>
);
expect(wrapper.find('FormSubmitError').text()).toEqual('');
});
test('should pass field errors to Formik', () => {
const error = {
response: {
data: {
name: 'invalid',
},
},
};
const wrapper = mountWithContexts(
<Formik>
{({ errors }) => (
<div>
<p>{errors.name}</p>
<FormSubmitError error={error} />
</div>
)}
</Formik>
);
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(
<Formik>{() => <FormSubmitError error={error} />}</Formik>
);
});
wrapper.update();
expect(wrapper.find('Alert').prop('title')).toEqual('There was an error');
expect(global.console.error).toHaveBeenCalledWith(error);
global.console = realConsole;
});
});

View File

@ -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';

View File

@ -33,8 +33,11 @@ function HostAdd() {
return (
<CardBody>
<HostForm handleSubmit={handleSubmit} handleCancel={handleCancel} />
{formError ? <div>error</div> : ''}
<HostForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
submitError={formError}
/>
</CardBody>
);
}

View File

@ -30,6 +30,12 @@ describe('<HostAdd />', () => {
});
test('handleSubmit should post to api', async () => {
HostsAPI.create.mockResolvedValueOnce({
data: {
...hostData,
id: 5,
},
});
await act(async () => {
wrapper.find('HostForm').prop('handleSubmit')(hostData);
});

View File

@ -45,8 +45,8 @@ function HostEdit({ host }) {
host={host}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
submitError={formError}
/>
{formError ? <div>error</div> : null}
</CardBody>
);
}
@ -55,5 +55,4 @@ HostEdit.propTypes = {
host: PropTypes.shape().isRequired,
};
export { HostEdit as _HostEdit };
export default HostEdit;

View File

@ -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`)}
/>
</FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
@ -99,6 +100,7 @@ HostForm.propTypes = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
host: shape({}),
submitError: shape({}),
};
HostForm.defaultProps = {
@ -111,6 +113,7 @@ HostForm.defaultProps = {
inventory: null,
},
},
submitError: null,
};
export { HostForm as _HostForm };

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { PageSection, Card } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import { InventoriesAPI, CredentialTypesAPI } from '@api';
@ -69,17 +68,6 @@ function InventoryAdd() {
}
};
if (error) {
return (
<PageSection>
<Card>
<CardBody>
<ContentError error={error} />
</CardBody>
</Card>
</PageSection>
);
}
if (isLoading) {
return <ContentLoading />;
}
@ -91,6 +79,7 @@ function InventoryAdd() {
onCancel={handleCancel}
onSubmit={handleSubmit}
credentialTypeId={credentialTypeId}
submitError={error}
/>
</CardBody>
</Card>

View File

@ -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 <ContentLoading />;
}
if (error) {
return <ContentError />;
}
return (
<CardBody>
<InventoryForm
@ -117,6 +112,7 @@ function InventoryEdit({ inventory }) {
inventory={inventory}
instanceGroups={associatedInstanceGroups}
credentialTypeId={credentialTypeId}
submitError={error}
/>
</CardBody>
);

View File

@ -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({
/>
</FormRow>
<FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={onCancel}
onSubmit={formik.handleSubmit}
@ -146,11 +148,13 @@ InventoryForm.proptype = {
instanceGroups: shape(),
inventory: shape(),
credentialTypeId: number.isRequired,
submitError: shape(),
};
InventoryForm.defaultProps = {
inventory: {},
instanceGroups: [],
submitError: null,
};
export default withI18n()(InventoryForm);

View File

@ -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
initialValues={{
name: host.name,
description: host.description,
variables: host.variables,
}}
onSubmit={handleSubmit}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<FormField
id="host-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
/>
<FormField
id="host-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
</FormRow>
<FormRow>
<VariablesField
id="host-variables"
name="variables"
label={i18n._(t`Variables`)}
/>
</FormRow>
<FormActionGroup
onCancel={handleCancel}
onSubmit={() => {
formik.handleSubmit();
}}
/>
</Form>
)}
</Formik>
);
}
InventoryHostForm.propTypes = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
host: shape({}),
};
InventoryHostForm.defaultProps = {
host: {
name: '',
description: '',
variables: '---\n',
},
};
export default withI18n()(InventoryHostForm);

View File

@ -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('<InventoryHostform />', () => {
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(
<InventoryHostForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
host={mockHostData}
/>
);
});
});
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();
});
});

View File

@ -40,10 +40,10 @@ function OrganizationAdd() {
onSubmit={handleSubmit}
onCancel={handleCancel}
me={me || {}}
submitError={formError}
/>
)}
</Config>
{formError ? <div>error</div> : ''}
</CardBody>
</Card>
</PageSection>

View File

@ -14,6 +14,7 @@ describe('<OrganizationAdd />', () => {
description: 'new description',
custom_virtualenv: 'Buzz',
};
OrganizationsAPI.create.mockResolvedValueOnce({ data: {} });
await act(async () => {
const wrapper = mountWithContexts(<OrganizationAdd />);
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);

View File

@ -48,10 +48,10 @@ function OrganizationEdit({ organization }) {
onSubmit={handleSubmit}
onCancel={handleCancel}
me={me || {}}
submitError={formError}
/>
)}
</Config>
{formError ? <div>error</div> : null}
</CardBody>
);
}

View File

@ -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 error={submitError} />
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
@ -179,6 +187,7 @@ OrganizationForm.propTypes = {
organization: PropTypes.shape(),
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
submitError: PropTypes.shape(),
};
OrganizationForm.defaultProps = {
@ -188,6 +197,7 @@ OrganizationForm.defaultProps = {
max_hosts: '0',
custom_virtualenv: '',
},
submitError: null,
};
OrganizationForm.contextTypes = {

View File

@ -41,13 +41,9 @@ function ProjectAdd() {
<ProjectForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
</CardBody>
{formSubmitError ? (
<div className="formSubmitError">formSubmitError</div>
) : (
''
)}
</Card>
</PageSection>
);

View File

@ -106,7 +106,8 @@ describe('<ProjectAdd />', () => {
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(<ProjectAdd />, {
context: { config },
@ -121,7 +122,7 @@ describe('<ProjectAdd />', () => {
});
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 () => {

View File

@ -42,13 +42,9 @@ function ProjectEdit({ project }) {
project={project}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
</CardBody>
{formSubmitError ? (
<div className="formSubmitError">formSubmitError</div>
) : (
''
)}
</Card>
);
}

View File

@ -120,7 +120,10 @@ describe('<ProjectEdit />', () => {
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(
<ProjectEdit project={{ ...projectData, scm_type: 'manual' }} />,
@ -135,7 +138,8 @@ describe('<ProjectEdit />', () => {
});
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 () => {

View File

@ -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 }) {
}
</Config>
</FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
@ -401,10 +405,12 @@ ProjectForm.propTypes = {
handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
project: PropTypes.shape({}),
submitError: PropTypes.shape({}),
};
ProjectForm.defaultProps = {
project: {},
submitError: null,
};
export default withI18n()(ProjectForm);

View File

@ -12,7 +12,7 @@ class TeamAdd extends React.Component {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.state = { error: '' };
this.state = { error: null };
}
async handleSubmit(values) {
@ -43,10 +43,10 @@ class TeamAdd extends React.Component {
handleSubmit={this.handleSubmit}
handleCancel={this.handleCancel}
me={me || {}}
submitError={error}
/>
)}
</Config>
{error ? <div>error</div> : ''}
</CardBody>
</Card>
</PageSection>

View File

@ -9,6 +9,7 @@ jest.mock('@api');
describe('<TeamAdd />', () => {
test('handleSubmit should post to api', async () => {
TeamsAPI.create.mockResolvedValueOnce({ data: {} });
const wrapper = mountWithContexts(<TeamAdd />);
const updatedTeamData = {
name: 'new name',

View File

@ -34,10 +34,10 @@ function TeamEdit({ team }) {
handleSubmit={handleSubmit}
handleCancel={handleCancel}
me={me || {}}
submitError={error}
/>
)}
</Config>
{error ? <div>error</div> : null}
</CardBody>
);
}

View File

@ -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) {
)}
</Field>
</FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
@ -84,10 +85,12 @@ TeamForm.propTypes = {
handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
team: PropTypes.shape({}),
submitError: PropTypes.shape(),
};
TeamForm.defaultProps = {
team: {},
submitError: null,
};
export default withI18n()(TeamForm);

View File

@ -66,9 +66,9 @@ function JobTemplateAdd() {
<JobTemplateForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
</CardBody>
{formSubmitError ? <div>formSubmitError</div> : ''}
</Card>
);
}

View File

@ -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 ? <div> error </div> : null}
</CardBody>
);
}

View File

@ -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 <ContentError error={contentError} />;
}
const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div';
return (
<Form autoComplete="off" onSubmit={handleSubmit}>
@ -585,6 +593,7 @@ class JobTemplateForm extends Component {
</FormRow>
</div>
</AdvancedFieldsWrapper>
<FormSubmitError error={submitError} />
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
</Form>
);
@ -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 };

View File

@ -35,13 +35,12 @@ function UserAdd() {
<PageSection>
<Card>
<CardBody>
<UserForm handleCancel={handleCancel} handleSubmit={handleSubmit} />
<UserForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
</CardBody>
{formSubmitError ? (
<div className="formSubmitError">formSubmitError</div>
) : (
''
)}
</Card>
</PageSection>
);

View File

@ -13,6 +13,7 @@ describe('<UserAdd />', () => {
await act(async () => {
wrapper = mountWithContexts(<UserAdd />);
});
UsersAPI.create.mockResolvedValueOnce({ data: {} });
const updatedUserData = {
username: 'sysadmin',
email: 'sysadmin@ansible.com',

View File

@ -27,8 +27,8 @@ function UserEdit({ user, history }) {
user={user}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
{formSubmitError ? <div> error </div> : null}
</CardBody>
);
}

View File

@ -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) {
}}
</Field>
</FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}