Skip to content

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

Merged
merged 10 commits into from
Dec 6, 2024

Conversation

luixlive
Copy link
Contributor

@luixlive luixlive commented Nov 28, 2024

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

image

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:

  • Update all our Docker files to use Node 20 (this required finally getting rid of that old and deprecated node-sass library and install sass instead).
  • Updating MSW from version 0 to version 2 required to use the new http API instead of rest, having to do changes in several test files. Also, I needed to add new polyfills to the setupTests because Jest famous jest-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.
  • Some libraries required changes in the way we do imports (mainly because they moved to ES modules) opting for a centralized package where to do named imports. These libraries are:
    • react-router
    • turf
    • react-error-boundary
    • redux-thunk
    • `uuid``
  • react-toastify changed the properties under the object toast 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 the ErrorBoundary component API, I just had to tweak a bit the fallback component there.
  • react-to-print changed the way you pass down the content ref.

Thus, my recommendations to start reviewing are:

Any background context you want to provide(if applicable)

  • AJV is a library that compiles JSON schemas into Javascript code that runs validations against objects that should follow the schema definition. This PR creates a hook that receives a schema and uses memoization to provide a method to run the validations over the compiled validator and returns a flat error object that maps the errors of each field by its id. Errors come as internationalized messages.
  • The dependencies update is a task we as frontend try to do once in a while. We haven't done it properly for more than a year so I did an update here. This one is specially a big one because I updated Node from 14 to 20, which required certain changes in some old libraries. For this reason, this PR requires a QA regression to make sure all features still work properly.

Future work:

  • A few dev dependencies weren't updated.
  • The new Node LTS version is 22, it should be easy to move there from Node 20.
  • After the updates done here, there are a lot of new warnings:
    • PropTypes deprecation: we should remove all our prop typing
    • Redux selectors performance issues: the new version of react-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.
    • New version of Sass also provides a lot of warning when building the project about deprecations in our style files (we have a few too).

Note: The translations spreadsheet was updated with the new texts.

Copy link

swarmia bot commented Nov 28, 2024

@luixlive luixlive marked this pull request as draft November 28, 2024 22:47
@luixlive luixlive changed the base branch from develop to ERA-10484 November 28, 2024 22:47

export const SchemaFormContext = createContext(null);
Copy link
Contributor Author

@luixlive luixlive Nov 29, 2024

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);
Copy link
Contributor Author

@luixlive luixlive Nov 29, 2024

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>
Copy link
Contributor Author

@luixlive luixlive Nov 29, 2024

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]) => {
Copy link
Contributor Author

@luixlive luixlive Nov 29, 2024

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 }));
Copy link
Contributor Author

@luixlive luixlive Nov 29, 2024

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';
Copy link
Contributor Author

@luixlive luixlive Nov 29, 2024

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.

Base automatically changed from ERA-10484 to develop December 2, 2024 22:13
@luixlive luixlive added deploy and removed deploy labels Dec 3, 2024
@luixlive luixlive marked this pull request as ready for review December 3, 2024 22:33
@@ -1,54 +1,27 @@
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
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 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>
Copy link
Contributor Author

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';
Copy link
Contributor Author

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);
Copy link
Contributor Author

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';
Copy link
Contributor Author

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,
Copy link
Contributor Author

@luixlive luixlive Dec 4, 2024

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(),
Copy link
Contributor Author

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.

Copy link
Contributor

@gaboDev gaboDev left a 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);
Copy link
Contributor

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') };
Copy link
Contributor

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 ?

Copy link
Contributor Author

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.

Copy link
Contributor

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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 🫡

Copy link
Contributor Author

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!

@gaboDev
Copy link
Contributor

gaboDev commented Dec 5, 2024

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

@luixlive

@luixlive
Copy link
Contributor Author

luixlive commented Dec 5, 2024

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 😄

Copy link
Contributor

@gaboDev gaboDev left a comment

Choose a reason for hiding this comment

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

Great work 🫡

@luixlive
Copy link
Contributor Author

luixlive commented Dec 5, 2024

Ready for you guys @AlanCalvillo @fonzy911 and of course feel free to add more feedback regarding the UI @tiffanynwong

@luixlive luixlive merged commit 2070210 into develop Dec 6, 2024
2 checks passed
@luixlive luixlive deleted the ERA-10572 branch December 6, 2024 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants