-
Notifications
You must be signed in to change notification settings - Fork 0
ERA-10572: Support EFB schemas in ER > Add ajv validation library #1196
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
Conversation
✅ Linked to Task ERA-10572 · Support EFB schemas in ER > Add ajv validation library |
|
||
export const SchemaFormContext = createContext(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
This file is now just a Context object to import everywhere and avoid circular dependencies. It existed as a Provider component just for the purpose of handling the onFieldChange
method, but with the errors object, now it makes more sense to have that method in the parent SchemaForm
. So, having the provider as an independent component doesn't have sense anymore.
@@ -26,13 +26,13 @@ const TEXT_INPUT_TYPE_TO_INPUT = { [INPUT_TYPE.SHORT]: ShortTextInput, [INPUT_TY | |||
const TEXT_INPUT_TYPE_STYLES = { [INPUT_TYPE.SHORT]: styles.shortText, [INPUT_TYPE.LONG]: styles.longText }; | |||
|
|||
const Text = ({ id }) => { | |||
const { fields, formData, onFieldChange } = useContext(SchemaFormContext); | |||
const { fields, fieldErrors, fieldValues, onFieldChange } = useContext(SchemaFormContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Now we read from the context three flat objects: fields
, fieldErrors
and fieldValues
. They map the field UI details, its validation error and the current value it holds. The three of them are mapped by the id of the field. This makes accessing this information trivial and O(1) since we don't have to handle recursivity with collections.
@@ -50,17 +50,19 @@ const Text = ({ id }) => { | |||
}, [details.defaultInput, id, onFieldChange, value]); | |||
|
|||
return <div data-testid={`schema-form-text-field-${id}`}> | |||
<label className={styles.label} htmlFor={id}>{label}</label> | |||
<label className={`${styles.label} ${error ? styles.error : ''}`} htmlFor={id}>{label}</label> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Here I add styles for showing an error state in the text field.
|
||
const fields = useMemo(() => makeFieldsFromSchema(schema), [schema]); | ||
|
||
const fieldValues = useMemo(() => Object.entries(formData).reduce((accumulator, [fieldId, fieldValue]) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Here we memoize the field values in a flat object. Every time the form data is updated this object will too.
const newFormData = { ...formData, [fieldId]: value || undefined }; | ||
|
||
onFormDataChange(newFormData); | ||
setFieldErrors((fieldErrors) => ({ ...fieldErrors, [fieldId]: undefined })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
If a field value is changed we clear the error.
import isObject from 'lodash/isObject'; | ||
import metaSchemaDraft04 from 'ajv/lib/refs/json-schema-draft-04.json'; | ||
import metaSchemaDraft04 from 'ajv-draft-04/dist/refs/json-schema-draft-04.json'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
The latest version of AJV, which I installed, doesn't come with the draft 04. I had to install the ajv-draft-04
library where we can read the metaschema to still support validation of our legacy schemas.
@@ -1,54 +1,27 @@ | |||
import React from 'react'; | |||
import cloneDeep from 'lodash/cloneDeep'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test file was broken due to changes in the new versions of redux and react-bootstrap. I needed to redo most of the tests but I made sure to keep the coverage.
@@ -50,20 +50,28 @@ const Text = ({ id }) => { | |||
}, [details.defaultInput, id, onFieldChange, value]); | |||
|
|||
return <div data-testid={`schema-form-text-field-${id}`}> | |||
<label className={styles.label} htmlFor={id}>{label}</label> | |||
<label className={`${styles.label} ${hasError ? styles.error : ''}`} htmlFor={id}>{label}</label> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
I added error state to the input and the corresponding aria tags to make the field accessible.
@@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; | |||
|
|||
import { FORM_ELEMENT_TYPES, ROOT_CANVAS_ID, TEXT_ELEMENT_INPUT_TYPES } from '../../constants'; | |||
import { render, screen } from '../../../../../test-utils'; | |||
import { SchemaFormContext } from '../../SchemaFormContext'; | |||
import SchemaFormContext from '../../SchemaFormContext'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Tests updated with the addition of the error state and the aria tags.
|
||
const onSubmit = (event) => { | ||
event.preventDefault(); | ||
|
||
// TODO: Add form validation with AJV before submission and show errors. | ||
onFormSubmit({ formData }); | ||
const fieldErrors = runValidations(formData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Before confirming the submission, we run the validations and show errors if there are any.
@@ -0,0 +1,34 @@ | |||
import { useCallback, useMemo } from 'react'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Hook that receives an schema, memoizes the AJV compilation of its validate function and returns a runValidations
memoized method that validates a form data object against the schema and returns a flat errors object mapping an internationalized message per error by the id of the field:
const runValidations = useSchemaValidations(schema);
...
const errors = runValidations(formData);
if (errors) {
// errors is a flat object like { fieldId: 'Meaningful internationalized message with the error!' }
} else {
// No errors!
}
formValidator, | ||
isCollection, | ||
loadingSchema, | ||
onFormChange, | ||
onFormDataChange, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
I added the new callback to update the form data, and renamed the old one to onLegacyFormChange
since we had to change a bit the logic of how we process the changes. They still do essentially the same thing.
import { report } from '../../__test-helpers/fixtures/reports'; | ||
import { VALID_EVENT_GEOMETRY_TYPES } from '../../constants'; | ||
|
||
import DetailsSection from './'; | ||
|
||
describe('ReportManager - DetailsSection', () => { | ||
const onFormChange = jest.fn(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to AJV validations
Had to redo (and added a few missing) most tests after the changes.
…nused comments from a test file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ready for approve, just asked some questions 🫡
const validate = useMemo(() => ajv.compile(schema.json), [schema.json]); | ||
|
||
const runValidations = useCallback((formData) => { | ||
const valid = validate(formData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
valid
is only being used right in the next line for the if statement, Can we remove it and put it directly into the condition?
if (!valid) { | ||
const fieldErrors = validate.errors.reduce((accumulator, error) => { | ||
if (error.keyword === 'required') { | ||
return { ...accumulator, [error.params.missingProperty]: t('required') }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, by using error.params.missingProperty
for the error mapping it means We are going to have the same generic error message for different field types, right?
I'm just asking, because idk if there might be some fields requiring more custom error messages ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It means we will have the same error message for the same type of error. In this case, it is the error required. Of course, we can add more complexity to the condition that renders the text, in case there is a very specific scenario for some kind of field that wants a different message for the same error. We can totally handle that by passing the field type to the hook 😄 But honestly, I don't think that will happen much, or maybe even at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds great!
const fieldErrors = runValidations(formData); | ||
if (fieldErrors) { | ||
setFieldErrors(fieldErrors); | ||
document.getElementById(Object.keys(fieldErrors)[0]).focus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This auto focus feels kind of non-react stuff, using a regular ref was complicating things?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain a bit more what do you mean by non-react? About using refs, we would need a ref for every field, and we don't know how many fields there are. That is dynamic depending on the schema. Maybe I can refactor it to be a little bit more legible:
const idOfFirstFieldWithError = Object.keys(fieldErrors)[0];
document.getElementById(idOfFirstFieldWithError).focus();
I don't know if that helps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant non-react by accessing directly to a node using document.getElementById
When usually We do it with refs, and as you mentioned it, having refs it would cause having unnecessary stuff around all components, so yes, I agree with the little refactor 🫡
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got you. Yeah it's not common to get elements by id in React. But in this specific scenario is pretty handy since we can't have a fixed number of refs because the fields are dynamic. I'll send the changes soon!
Do We have tickets created already for the points you mentioned on the future work section? If not, it would be great to have them as tech debt tasks |
You are right Gabo! I will create them 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work 🫡
Ready for you guys @AlanCalvillo @fonzy911 and of course feel free to add more feedback regarding the UI @tiffanynwong |
What does this PR do?
Add AJV validations to the new SchemaForm fields on submission and show an error state in the fields.
This PR also updates the project to use Node 20 and its dependencies with exception of dev dependencies (this PR is already too big).
How does it look
Relevant link(s)
Where / how to start reviewing (optional)
I added comments that start with Related to AJV validations in the files where I added the new form validations to make it easier to review. These comments explain the reasoning behind each change. The rest of the files (> 85%) only have minor changes related to the dependency updates. In order to make the review process, I mention here what changes needed to be done after the updates:
node-sass
library and installsass
instead).http
API instead ofrest
, having to do changes in several test files. Also, I needed to add new polyfills to the setupTests because Jest famousjest-environment-jsdom
doesn't include a few. Actually, seems like the community talks about moving from Jest to Vitest since its faster, easier and has better polyfills for newer apps. But that is a conversation for another day.react-router
turf
react-error-boundary
redux-thunk
react-toastify
changed the properties under the objecttoast
and removed several utilities that now we have to write like the toast type.turf
also updated some of the math calculations it does to provide more exact areas and perimeters, so some tests had to be updated to match the new values.react-error-boundary
changed a few things of theErrorBoundary
component API, I just had to tweak a bit the fallback component there.react-to-print
changed the way you pass down thecontent
ref.Thus, my recommendations to start reviewing are:
Any background context you want to provide(if applicable)
Future work:
PropTypes
deprecation: we should remove all our prop typingreact-redux
comes with helpful warnings to see what selectors can be improved to avoid rerenders. Sadly, we have a big bunch of places to optimize. But it's good to have that information and start working on it.Note: The translations spreadsheet was updated with the new texts.