diff --git a/src/ReportForm/ErrorMessages.js b/src/ReportForm/ErrorMessages.js index 9abf300f2..b268e4ceb 100644 --- a/src/ReportForm/ErrorMessages.js +++ b/src/ReportForm/ErrorMessages.js @@ -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 -
Error saving report: {errorData.message}
- {} + + return + + Error saving report. + + See details + + + + ; }; export default memo(ReportFormErrorMessages); ReportFormErrorMessages.propTypes = { - errorData: PropTypes.object.isRequired, + errorData: PropTypes.array.isRequired, onClose: PropTypes.func, }; \ No newline at end of file diff --git a/src/ReportForm/ErrorMessages.test.js b/src/ReportForm/ErrorMessages.test.js new file mode 100644 index 000000000..85711f512 --- /dev/null +++ b/src/ReportForm/ErrorMessages.test.js @@ -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(); +}); + +describe('Error messages', () => { + beforeEach(async () => { + render(); + }); + + 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(); + + userEvent.click(detailsButton); + notExpandedAccordion = screen.getByRole('menuitem', { expanded: false }); + expect(notExpandedAccordion).toBeTruthy(); + }); + + 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); + + }); +}); diff --git a/src/ReportForm/ReportFormBody.js b/src/ReportForm/ReportFormBody.js index daa547133..4f2b4c701 100644 --- a/src/ReportForm/ReportFormBody.js +++ b/src/ReportForm/ReportFormBody.js @@ -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); let match; if (linearErrorPropTree.length === 1) { @@ -30,7 +33,7 @@ const filterOutEnumErrors = (errors, schema) => errors // filter out enum-based }, schema); } - return !!match && !match.enum; + return !!match || match?.enum?.length; }); const filterOutRequiredValueOnSchemaPropErrors = errors => errors.filter(err => !JSON.stringify(err).includes('required should be array')); @@ -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] ); @@ -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} ; }); diff --git a/src/ReportForm/index.js b/src/ReportForm/index.js index 1b79edfda..6edf77f29 100644 --- a/src/ReportForm/index.js +++ b/src/ReportForm/index.js @@ -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'; @@ -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]); @@ -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(); @@ -449,7 +458,6 @@ const ReportForm = (props) => { return {saving && } - {saveError && }
{ title={reportTitle} onTitleChange={onReportTitleChange} /> + {saveError && }
@@ -490,6 +499,7 @@ const ReportForm = (props) => { formScrollContainer={scrollContainerRef.current} onChange={onDetailChange} onSubmit={startSave} + onError={showError} schema={schema} uiSchema={uiSchema}>