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
35 changed files with 206 additions and 207 deletions

View File

@@ -53,8 +53,13 @@ class ErrorDetail extends Component {
const { error } = this.props; const { error } = this.props;
const { response } = error; const { response } = error;
const message = let message = '';
typeof response.data === 'string' ? response.data : response.data.detail; if (response.data) {
message =
typeof response.data === 'string'
? response.data
: response.data?.detail;
}
return ( return (
<Fragment> <Fragment>

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ActionGroup as PFActionGroup, Button } from '@patternfly/react-core'; import { ActionGroup as PFActionGroup, Button } from '@patternfly/react-core';
import styled from 'styled-components';
const ActionGroup = styled(PFActionGroup)` const ActionGroup = styled(PFActionGroup)`
display: flex; display: flex;
@@ -11,14 +11,13 @@ const ActionGroup = styled(PFActionGroup)`
--pf-c-form__group--m-action--MarginTop: 0; --pf-c-form__group--m-action--MarginTop: 0;
.pf-c-form__actions { .pf-c-form__actions {
display: grid;
gap: 24px;
grid-template-columns: auto auto;
margin: 0;
& > button { & > button {
margin: 0; margin: 0;
} }
& > :not(:first-child) {
margin-left: 24px;
}
} }
`; `;

View File

@@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
import FormActionGroup from './FormActionGroup'; import FormActionGroup from './FormActionGroup';
describe('FormActionGroup', () => { describe('FormActionGroup', () => {
test('renders the expected content', () => { test('should render the expected content', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<FormActionGroup onSubmit={() => {}} onCancel={() => {}} /> <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 CheckboxField } from './CheckboxField';
export { default as FieldTooltip } from './FieldTooltip'; export { default as FieldTooltip } from './FieldTooltip';
export { default as PasswordField } from './PasswordField'; export { default as PasswordField } from './PasswordField';
export { default as FormSubmitError } from './FormSubmitError';

View File

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

View File

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

View File

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

View File

@@ -9,13 +9,13 @@ import { t } from '@lingui/macro';
import { Form } from '@patternfly/react-core'; import { Form } from '@patternfly/react-core';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import FormField from '@components/FormField'; import FormField, { FormSubmitError } from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import { VariablesField } from '@components/CodeMirrorInput'; import { VariablesField } from '@components/CodeMirrorInput';
import { required } from '@util/validators'; import { required } from '@util/validators';
import { InventoryLookup } from '@components/Lookup'; import { InventoryLookup } from '@components/Lookup';
function HostForm({ handleSubmit, handleCancel, host, i18n }) { function HostForm({ handleSubmit, handleCancel, host, submitError, i18n }) {
const [inventory, setInventory] = useState( const [inventory, setInventory] = useState(
host ? host.summary_fields.inventory : '' host ? host.summary_fields.inventory : ''
); );
@@ -85,6 +85,7 @@ function HostForm({ handleSubmit, handleCancel, host, i18n }) {
label={i18n._(t`Variables`)} label={i18n._(t`Variables`)}
/> />
</FormRow> </FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -99,6 +100,7 @@ HostForm.propTypes = {
handleSubmit: func.isRequired, handleSubmit: func.isRequired,
handleCancel: func.isRequired, handleCancel: func.isRequired,
host: shape({}), host: shape({}),
submitError: shape({}),
}; };
HostForm.defaultProps = { HostForm.defaultProps = {
@@ -111,6 +113,7 @@ HostForm.defaultProps = {
inventory: null, inventory: null,
}, },
}, },
submitError: null,
}; };
export { HostForm as _HostForm }; export { HostForm as _HostForm };

View File

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

View File

@@ -5,7 +5,6 @@ import { object } from 'prop-types';
import { CardBody } from '@components/Card'; import { CardBody } from '@components/Card';
import { InventoriesAPI, CredentialTypesAPI } from '@api'; import { InventoriesAPI, CredentialTypesAPI } from '@api';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import ContentError from '@components/ContentError';
import InventoryForm from '../shared/InventoryForm'; import InventoryForm from '../shared/InventoryForm';
import { getAddedAndRemoved } from '../../../util/lists'; import { getAddedAndRemoved } from '../../../util/lists';
@@ -105,10 +104,6 @@ function InventoryEdit({ inventory }) {
return <ContentLoading />; return <ContentLoading />;
} }
if (error) {
return <ContentError />;
}
return ( return (
<CardBody> <CardBody>
<InventoryForm <InventoryForm
@@ -117,6 +112,7 @@ function InventoryEdit({ inventory }) {
inventory={inventory} inventory={inventory}
instanceGroups={associatedInstanceGroups} instanceGroups={associatedInstanceGroups}
credentialTypeId={credentialTypeId} credentialTypeId={credentialTypeId}
submitError={error}
/> />
</CardBody> </CardBody>
); );

View File

@@ -6,7 +6,7 @@ import { func, number, shape } from 'prop-types';
import { VariablesField } from '@components/CodeMirrorInput'; import { VariablesField } from '@components/CodeMirrorInput';
import { Form } from '@patternfly/react-core'; import { Form } from '@patternfly/react-core';
import FormField from '@components/FormField'; import FormField, { FormSubmitError } from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import { required } from '@util/validators'; import { required } from '@util/validators';
@@ -21,6 +21,7 @@ function InventoryForm({
onSubmit, onSubmit,
instanceGroups, instanceGroups,
credentialTypeId, credentialTypeId,
submitError,
}) { }) {
const initialValues = { const initialValues = {
name: inventory.name || '', name: inventory.name || '',
@@ -129,6 +130,7 @@ function InventoryForm({
/> />
</FormRow> </FormRow>
<FormRow> <FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={onCancel} onCancel={onCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -146,11 +148,13 @@ InventoryForm.proptype = {
instanceGroups: shape(), instanceGroups: shape(),
inventory: shape(), inventory: shape(),
credentialTypeId: number.isRequired, credentialTypeId: number.isRequired,
submitError: shape(),
}; };
InventoryForm.defaultProps = { InventoryForm.defaultProps = {
inventory: {}, inventory: {},
instanceGroups: [], instanceGroups: [],
submitError: null,
}; };
export default withI18n()(InventoryForm); 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} onSubmit={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
me={me || {}} me={me || {}}
submitError={formError}
/> />
)} )}
</Config> </Config>
{formError ? <div>error</div> : ''}
</CardBody> </CardBody>
</Card> </Card>
</PageSection> </PageSection>

View File

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

View File

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

View File

@@ -13,13 +13,20 @@ import AnsibleSelect from '@components/AnsibleSelect';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import FormField from '@components/FormField'; import FormField, { FormSubmitError } from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import { InstanceGroupsLookup } from '@components/Lookup/'; import { InstanceGroupsLookup } from '@components/Lookup/';
import { getAddedAndRemoved } from '@util/lists'; import { getAddedAndRemoved } from '@util/lists';
import { required, minMaxValue } from '@util/validators'; import { required, minMaxValue } from '@util/validators';
function OrganizationForm({ organization, i18n, me, onCancel, onSubmit }) { function OrganizationForm({
organization,
i18n,
me,
onCancel,
onSubmit,
submitError,
}) {
const defaultVenv = { const defaultVenv = {
label: i18n._(t`Use Default Ansible Environment`), label: i18n._(t`Use Default Ansible Environment`),
value: '/venv/ansible/', 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.` t`Select the Instance Groups for this Organization to run on.`
)} )}
/> />
<FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -179,6 +187,7 @@ OrganizationForm.propTypes = {
organization: PropTypes.shape(), organization: PropTypes.shape(),
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
submitError: PropTypes.shape(),
}; };
OrganizationForm.defaultProps = { OrganizationForm.defaultProps = {
@@ -188,6 +197,7 @@ OrganizationForm.defaultProps = {
max_hosts: '0', max_hosts: '0',
custom_virtualenv: '', custom_virtualenv: '',
}, },
submitError: null,
}; };
OrganizationForm.contextTypes = { OrganizationForm.contextTypes = {

View File

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

View File

@@ -106,7 +106,8 @@ describe('<ProjectAdd />', () => {
project_local_paths: ['foobar', 'qux'], project_local_paths: ['foobar', 'qux'],
project_base_dir: 'dir/foo/bar', 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 () => { await act(async () => {
wrapper = mountWithContexts(<ProjectAdd />, { wrapper = mountWithContexts(<ProjectAdd />, {
context: { config }, context: { config },
@@ -121,7 +122,7 @@ describe('<ProjectAdd />', () => {
}); });
wrapper.update(); wrapper.update();
expect(ProjectsAPI.create).toHaveBeenCalledTimes(1); 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 () => { test('CardBody cancel button should navigate to projects list', async () => {

View File

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

View File

@@ -120,7 +120,10 @@ describe('<ProjectEdit />', () => {
project_local_paths: [], project_local_paths: [],
project_base_dir: 'foo/bar', 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 () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ProjectEdit project={{ ...projectData, scm_type: 'manual' }} />, <ProjectEdit project={{ ...projectData, scm_type: 'manual' }} />,
@@ -135,7 +138,8 @@ describe('<ProjectEdit />', () => {
}); });
wrapper.update(); wrapper.update();
expect(ProjectsAPI.update).toHaveBeenCalledTimes(1); 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 () => { 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 ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; 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 FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { CredentialTypesAPI, ProjectsAPI } from '@api'; 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 { i18n, handleCancel, handleSubmit } = props;
const { summary_fields = {} } = project; const { summary_fields = {} } = project;
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
@@ -385,6 +388,7 @@ function ProjectForm({ project, ...props }) {
} }
</Config> </Config>
</FormRow> </FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -401,10 +405,12 @@ ProjectForm.propTypes = {
handleCancel: PropTypes.func.isRequired, handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
project: PropTypes.shape({}), project: PropTypes.shape({}),
submitError: PropTypes.shape({}),
}; };
ProjectForm.defaultProps = { ProjectForm.defaultProps = {
project: {}, project: {},
submitError: null,
}; };
export default withI18n()(ProjectForm); export default withI18n()(ProjectForm);

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,13 @@ import { t } from '@lingui/macro';
import { Formik, Field } from 'formik'; import { Formik, Field } from 'formik';
import { Form } from '@patternfly/react-core'; import { Form } from '@patternfly/react-core';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormField from '@components/FormField'; import FormField, { FormSubmitError } from '@components/FormField';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required } from '@util/validators'; import { required } from '@util/validators';
function TeamForm(props) { function TeamForm(props) {
const { team, handleCancel, handleSubmit, i18n } = props; const { team, handleCancel, handleSubmit, submitError, i18n } = props;
const [organization, setOrganization] = useState( const [organization, setOrganization] = useState(
team.summary_fields ? team.summary_fields.organization : null team.summary_fields ? team.summary_fields.organization : null
); );
@@ -70,6 +70,7 @@ function TeamForm(props) {
)} )}
</Field> </Field>
</FormRow> </FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -84,10 +85,12 @@ TeamForm.propTypes = {
handleCancel: PropTypes.func.isRequired, handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
team: PropTypes.shape({}), team: PropTypes.shape({}),
submitError: PropTypes.shape(),
}; };
TeamForm.defaultProps = { TeamForm.defaultProps = {
team: {}, team: {},
submitError: null,
}; };
export default withI18n()(TeamForm); export default withI18n()(TeamForm);

View File

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

View File

@@ -113,8 +113,8 @@ class JobTemplateEdit extends Component {
this.submitCredentials(credentials), this.submitCredentials(credentials),
]); ]);
history.push(this.detailsUrl); history.push(this.detailsUrl);
} catch (formSubmitError) { } catch (error) {
this.setState({ formSubmitError }); this.setState({ formSubmitError: error });
} }
} }
@@ -209,8 +209,8 @@ class JobTemplateEdit extends Component {
handleCancel={this.handleCancel} handleCancel={this.handleCancel}
handleSubmit={this.handleSubmit} handleSubmit={this.handleSubmit}
relatedProjectPlaybooks={relatedProjectPlaybooks} relatedProjectPlaybooks={relatedProjectPlaybooks}
submitError={formSubmitError}
/> />
{formSubmitError ? <div> error </div> : null}
</CardBody> </CardBody>
); );
} }

View File

@@ -16,7 +16,11 @@ import ContentLoading from '@components/ContentLoading';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import { TagMultiSelect } from '@components/MultiSelect'; import { TagMultiSelect } from '@components/MultiSelect';
import FormActionGroup from '@components/FormActionGroup'; 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 FormRow from '@components/FormRow';
import CollapsibleSection from '@components/CollapsibleSection'; import CollapsibleSection from '@components/CollapsibleSection';
import { required } from '@util/validators'; import { required } from '@util/validators';
@@ -48,6 +52,7 @@ class JobTemplateForm extends Component {
template: JobTemplate, template: JobTemplate,
handleCancel: PropTypes.func.isRequired, handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
submitError: PropTypes.shape({}),
}; };
static defaultProps = { static defaultProps = {
@@ -66,6 +71,7 @@ class JobTemplateForm extends Component {
}, },
isNew: true, isNew: true,
}, },
submitError: null,
}; };
constructor(props) { constructor(props) {
@@ -161,8 +167,9 @@ class JobTemplateForm extends Component {
handleSubmit, handleSubmit,
handleBlur, handleBlur,
setFieldValue, setFieldValue,
i18n,
template, template,
submitError,
i18n,
} = this.props; } = this.props;
const jobTypeOptions = [ const jobTypeOptions = [
@@ -201,6 +208,7 @@ class JobTemplateForm extends Component {
if (contentError) { if (contentError) {
return <ContentError error={contentError} />; return <ContentError error={contentError} />;
} }
const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div'; const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div';
return ( return (
<Form autoComplete="off" onSubmit={handleSubmit}> <Form autoComplete="off" onSubmit={handleSubmit}>
@@ -585,6 +593,7 @@ class JobTemplateForm extends Component {
</FormRow> </FormRow>
</div> </div>
</AdvancedFieldsWrapper> </AdvancedFieldsWrapper>
<FormSubmitError error={submitError} />
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} /> <FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
</Form> </Form>
); );
@@ -631,7 +640,13 @@ const FormikApp = withFormik({
credentials: summary_fields.credentials || [], 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); })(JobTemplateForm);
export { JobTemplateForm as _JobTemplateForm }; export { JobTemplateForm as _JobTemplateForm };

View File

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

View File

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

View File

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

View File

@@ -6,13 +6,15 @@ import { Formik, Field } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form, FormGroup } from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; 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 FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required, requiredEmail } from '@util/validators'; import { required, requiredEmail } from '@util/validators';
function UserForm(props) { function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) {
const { user, handleCancel, handleSubmit, i18n } = props;
const [organization, setOrganization] = useState(null); const [organization, setOrganization] = useState(null);
const userTypeOptions = [ const userTypeOptions = [
@@ -183,6 +185,7 @@ function UserForm(props) {
}} }}
</Field> </Field>
</FormRow> </FormRow>
<FormSubmitError error={submitError} />
<FormActionGroup <FormActionGroup
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}