Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DAS-7055, DAS-7064 Improve feedback errors for missing fields in report forms #496

Merged
merged 11 commits into from
Oct 13, 2021
31 changes: 20 additions & 11 deletions src/ReportForm/ErrorMessages.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import Accordion from 'react-bootstrap/Accordion';
import Button from 'react-bootstrap/Button';
import Alert from 'react-bootstrap/Alert';

import { generateErrorListForApiResponseDetails } from '../utils/events';

import styles from './styles.module.scss';

const ReportFormErrorMessages = (props) => {
const { errorData, onClose } = props;
return <Alert onClose={onClose} dismissible={true} className={styles.saveErrorAlert} variant='danger'>
<h5>Error saving report: {errorData.message}</h5>
{<ul>
{generateErrorListForApiResponseDetails(errorData).map(item =>
<li key={`${item.label} ${item.message}`}>
<strong>{item.label}</strong>: <span>{item.message}</span>
</li>)}
</ul>}

return <Alert onClose={onClose} dismissible={true} className={styles.saveErrorAlert} data-testid='errors-alert' >
<Accordion>
<span>Error saving report.</span>
<Accordion.Toggle as={Button} variant="link" eventKey="0" className={styles.saveErrorAlertLink} data-testid='error-details-btn'>
See details
</Accordion.Toggle>

<Accordion.Collapse eventKey="0" role="menuitem" aria-expanded='false' >
<ul>
{errorData.map(item =>
<li key={`${item.label} ${item.message}`} data-testid='error-message'>
<strong>{item.label}</strong>: <span>{item.message}</span>
</li>)}
</ul>
</Accordion.Collapse>
</Accordion>
</Alert>;
};

export default memo(ReportFormErrorMessages);

ReportFormErrorMessages.propTypes = {
errorData: PropTypes.object.isRequired,
errorData: PropTypes.array.isRequired,
onClose: PropTypes.func,
};
86 changes: 86 additions & 0 deletions src/ReportForm/ErrorMessages.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import ErrorMessages from './ErrorMessages';

const ERROR_DATA = [{
'name': 'required',
'property': '.reportnationalpark_enum',
'message': 'is a required property',
'stack': '.reportnationalpark_enum is a required property',
'linearProperty': [
'reportnationalpark_enum'
],
'label': 'National Park'
},
{
'name': 'required',
'property': '.reportinternal',
'message': 'is a required property',
'stack': '.reportinternal is a required property',
'linearProperty': [
'reportinternal'
],
'label': 'TEST FieldSet Checkbox Enum from definition'
},
{
'name': 'required',
'property': '.blackRhinos',
'message': 'is a required property',
'stack': '.blackRhinos is a required property',
'linearProperty': [
'blackRhinos'
],
'label': 'checkbox TEST with query'
}];
const clearErrors = jest.fn();

test('rendering without crashing', () => {
render(<ErrorMessages onClose={clearErrors} errorData={ERROR_DATA} />);
});

describe('Error messages', () => {
beforeEach(async () => {
render(<ErrorMessages onClose={clearErrors} errorData={ERROR_DATA} />);
});

afterEach(() => {
clearErrors.mockClear();
});


test('it should format the errors with the label of the form field followed by the error message', () => {
const sortOptionsContainer = screen.queryAllByTestId('error-message');

// example: "National Park: is a required property"
expect(sortOptionsContainer[0].textContent).toEqual(`${ERROR_DATA[0].label}: ${ERROR_DATA[0].message}`);
expect(sortOptionsContainer[1].textContent).toEqual(`${ERROR_DATA[1].label}: ${ERROR_DATA[1].message}`);
expect(sortOptionsContainer[2].textContent).toEqual(`${ERROR_DATA[2].label}: ${ERROR_DATA[2].message}`);
});

test('The errors list should be hidden, but displayed only if the user clicks on see details', async () => {
const detailsButton = await screen.getByTestId('error-details-btn');
let notExpandedAccordion = screen.getByRole('menuitem', { expanded: false });
expect(notExpandedAccordion).toBeTruthy();

userEvent.click(detailsButton);

const expandedAccordion = screen.getByRole('menuitem', { expanded: true });
expect(expandedAccordion).toBeTruthy();
luixlive marked this conversation as resolved.
Show resolved Hide resolved

userEvent.click(detailsButton);
notExpandedAccordion = screen.getByRole('menuitem', { expanded: false });
expect(notExpandedAccordion).toBeTruthy();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better! 🎉

});

test('clicking on close icon should dismiss the alert', () => {
const errorAlert = screen.getByTestId('errors-alert');
const closeButton = within(errorAlert).getAllByRole('button');

userEvent.click(closeButton[0]);

expect(clearErrors).toHaveBeenCalledTimes(1);

});
});
32 changes: 16 additions & 16 deletions src/ReportForm/ReportFormBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import styles from './styles.module.scss';

const additionalMetaSchemas = [draft4JsonSchema];

const getLinearErrorPropTree = (errorProperty) => {
const nonPropAccessorNotations = /'|\.properties|\[|\]|\.enumNames|\.enum/g;
return errorProperty.replace(nonPropAccessorNotations, '.')
.split('.')
.filter(p => !!p)
.map(item => isNaN(item) ? item : parseFloat(item));
};

const filterOutEnumErrors = (errors, schema) => errors // filter out enum-based errors, as it's a type conflict between the property having type='string' when our API returns strings but expecting objects in the POSTs.
.filter((error) => {
const linearErrorPropTree = error.property
.replace(/'|\.properties|\[|\]|\.enumNames|\.enum/g, '.')
.split('.')
.filter(p => !!p)
.map(item => isNaN(item) ? item : parseFloat(item));

const linearErrorPropTree = getLinearErrorPropTree(error.property);
amicavi marked this conversation as resolved.
Show resolved Hide resolved
let match;

if (linearErrorPropTree.length === 1) {
Expand All @@ -30,7 +33,7 @@ const filterOutEnumErrors = (errors, schema) => errors // filter out enum-based
}, schema);
}

return !!match && !match.enum;
return !!match || match?.enum?.length;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was causing for some required fields to not show an error

});

const filterOutRequiredValueOnSchemaPropErrors = errors => errors.filter(err => !JSON.stringify(err).includes('required should be array'));
Expand All @@ -41,7 +44,10 @@ const ReportFormBody = forwardRef((props, ref) => { // eslint-disable-line react
const transformErrors = useCallback((errors) => {
const errs = filterOutRequiredValueOnSchemaPropErrors(
filterOutEnumErrors(errors, schema));
return errs;
return errs.map(err => ({
...err,
linearProperty: getLinearErrorPropTree(err.property)
}));
}, [schema]
);

Expand All @@ -51,21 +57,15 @@ const ReportFormBody = forwardRef((props, ref) => { // eslint-disable-line react
className={styles.form}
disabled={schema.readonly}
formData={formData}
liveValidate={true}
onChange={onChange}
formContext={
{
scrollContainer: formScrollContainer,
}
}
formContext={{ scrollContainer: formScrollContainer }}
onSubmit={onSubmit}
ref={ref}
schema={schema}
ObjectFieldTemplate={ObjectFieldTemplate}
transformErrors={transformErrors}
uiSchema={uiSchema}
{...rest}
>
{...rest}>
{children}
</Form>;
});
Expand Down
16 changes: 13 additions & 3 deletions src/ReportForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import LoadingOverlay from '../LoadingOverlay';
import { fetchImageAsBase64FromUrl, filterDuplicateUploadFilenames } from '../utils/file';
import { downloadFileFromUrl } from '../utils/download';
import { openModalForPatrol } from '../utils/patrols';
import { addPatrolSegmentToEvent, eventBelongsToCollection, eventBelongsToPatrol, createNewIncidentCollection, openModalForReport, displayTitleForEvent, eventTypeTitleForEvent } from '../utils/events';
import { addPatrolSegmentToEvent, eventBelongsToCollection, eventBelongsToPatrol, createNewIncidentCollection, openModalForReport, displayTitleForEvent, eventTypeTitleForEvent, generateErrorListForApiResponseDetails } from '../utils/events';
import { calcTopRatedReportAndTypeForCollection } from '../utils/event-types';
import { generateSaveActionsForReportLikeObject, executeSaveActions } from '../utils/save';
import { extractObjectDifference } from '../utils/objects';
Expand Down Expand Up @@ -86,7 +86,7 @@ const ReportForm = (props) => {

const handleSaveError = useCallback((e) => {
setSavingState(false);
setSaveErrorState(e);
setSaveErrorState(generateErrorListForApiResponseDetails(e));
onSaveError && onSaveError(e);
setTimeout(clearErrors, 7000);
}, [onSaveError]);
Expand Down Expand Up @@ -322,6 +322,15 @@ const ReportForm = (props) => {
setSavingState(true);
};

const showError = (err) => {
const formattedErrors = err.map(e => ({
...e,
label: schema?.properties?.[e.linearProperty]?.title ?? e.linearProperty,
}));

setSaveErrorState([...formattedErrors]);
};

const startSubmitForm = useCallback(() => {
if (is_collection) {
startSave();
Expand Down Expand Up @@ -449,7 +458,6 @@ const ReportForm = (props) => {
return <ContextProvider value={report}>

{saving && <LoadingOverlay message='Saving...' className={styles.loadingOverlay} />}
{saveError && <ReportFormErrorMessages onClose={clearErrors} errorData={saveError} />}

<Header
analyticsMetadata={{
Expand All @@ -462,6 +470,7 @@ const ReportForm = (props) => {
title={reportTitle} onTitleChange={onReportTitleChange}
/>

{saveError && <ReportFormErrorMessages onClose={clearErrors} errorData={saveError} />}
<div ref={reportedBySelectPortalRef} style={{ padding: 0 }}></div>

<Body ref={scrollContainerRef}>
Expand Down Expand Up @@ -490,6 +499,7 @@ const ReportForm = (props) => {
formScrollContainer={scrollContainerRef.current}
onChange={onDetailChange}
onSubmit={startSave}
onError={showError}
schema={schema}
uiSchema={uiSchema}>
<AttachmentList
Expand Down
24 changes: 18 additions & 6 deletions src/ReportForm/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,33 @@ div.datePopover {

.saveErrorAlert {
@include fade-in;
border-radius: 0;
bottom: -1rem;
box-shadow: 0 0 .25rem rgb(38, 38, 38);
min-height: 10rem;
position: absolute;
border-radius: 0 !important;
background-color: rgba(253, 242, 244, 0.95);
box-shadow: rgba(0, 0, 0, 0.45) 0rem 1.5rem 1.2rem -1.2rem;
color: $bright-red;
font-weight: 500;
position: absolute !important;
width: 100%;
z-index: 5;
h5 {
span {
margin-bottom: 1rem;
}
ul {
margin: 0;
}
}

.saveErrorAlertLink {
display: inline;
color: $bright-red !important;
padding-top: 0.22rem !important;
text-decoration: underline !important;

&:hover {
color: $bright-red !important;
}
}

.reportControls {
background: lighten($light-gray-background, 5%);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.65);
Expand Down