From caa59ca1797f2f3ef3fe53f16e9601e9f6ad7d7a Mon Sep 17 00:00:00 2001 From: Yasunobu Chiba <724165+chibacchie@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:25:58 +0900 Subject: [PATCH] Partially fixed issue where dependency errors do not show title or ui:title. This fix only applicable if we use an ajv-i18n localizer. Ref. #4402. --- CHANGELOG.md | 6 + package-lock.json | 1 + packages/validator-ajv8/package.json | 1 + .../src/processRawValidationErrors.ts | 52 ++-- packages/validator-ajv8/src/validator.ts | 28 ++- .../validator-ajv8/test/validator.test.ts | 235 ++++++++++++++++++ 6 files changed, 295 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b014249c06..d2d0c7e080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b --> +# 5.23.3 + +## @rjsf/validator-ajv8 + +- Partially fixed issue where dependency errors do not show `title` or `ui:title`. This fix only applicable if we use an ajv-i18n localizer. Ref. [#4402](https://github.com/rjsf-team/react-jsonschema-form/issues/4402). + # 5.23.2 ## @rjsf/core diff --git a/package-lock.json b/package-lock.json index e234e10a51..30d8d7b380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35330,6 +35330,7 @@ "@types/jest": "^29.5.12", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.14.202", + "ajv-i18n": "^4.2.0", "babel-jest": "^29.7.0", "eslint": "^8.56.0", "jest": "^29.7.0", diff --git a/packages/validator-ajv8/package.json b/packages/validator-ajv8/package.json index e628e83765..25baa0f7da 100644 --- a/packages/validator-ajv8/package.json +++ b/packages/validator-ajv8/package.json @@ -52,6 +52,7 @@ "@types/jest": "^29.5.12", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.14.202", + "ajv-i18n": "^4.2.0", "babel-jest": "^29.7.0", "eslint": "^8.56.0", "jest": "^29.7.0", diff --git a/packages/validator-ajv8/src/processRawValidationErrors.ts b/packages/validator-ajv8/src/processRawValidationErrors.ts index cf3d7411cd..b35e1a6a70 100644 --- a/packages/validator-ajv8/src/processRawValidationErrors.ts +++ b/packages/validator-ajv8/src/processRawValidationErrors.ts @@ -36,29 +36,33 @@ export function transformRJSFValidationErrors< let { message = '' } = rest; let property = instancePath.replace(/\//g, '.'); let stack = `${property} ${message}`.trim(); - - if ('missingProperty' in params) { - property = property ? `${property}.${params.missingProperty}` : params.missingProperty; - const currentProperty: string = params.missingProperty; - let uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title; - if (uiSchemaTitle === undefined) { - const uiSchemaPath = schemaPath - .replace(/\/properties\//g, '/') - .split('/') - .slice(1, -1) - .concat([currentProperty]); - uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title; - } - - if (uiSchemaTitle) { - message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`); - } else { - const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']); - - if (parentSchemaTitle) { - message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`); + const rawPropertyNames: string[] = [ + ...(params.deps?.split(', ') || []), + params.missingProperty, + params.property, + ].filter((item) => item); + + if (rawPropertyNames.length > 0) { + rawPropertyNames.forEach((currentProperty) => { + const path = property ? `${property}.${currentProperty}` : currentProperty; + let uiSchemaTitle = getUiOptions(get(uiSchema, `${path.replace(/^\./, '')}`)).title; + if (uiSchemaTitle === undefined) { + const uiSchemaPath = schemaPath + .replace(/\/properties\//g, '/') + .split('/') + .slice(1, -1) + .concat([currentProperty]); + uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title; } - } + if (uiSchemaTitle) { + message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`); + } else { + const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']); + if (parentSchemaTitle) { + message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`); + } + } + }); stack = message; } else { @@ -75,6 +79,10 @@ export function transformRJSFValidationErrors< } } + if ('missingProperty' in params) { + property = property ? `${property}.${params.missingProperty}` : params.missingProperty; + } + // put data in expected format return { name: keyword, diff --git a/packages/validator-ajv8/src/validator.ts b/packages/validator-ajv8/src/validator.ts index b0b0125662..82200c43ab 100644 --- a/packages/validator-ajv8/src/validator.ts +++ b/packages/validator-ajv8/src/validator.ts @@ -90,19 +90,35 @@ export default class AJV8Validator { - if (error.params?.missingProperty) { - error.params.missingProperty = `'${error.params.missingProperty}'`; + ['missingProperty', 'property'].forEach((key) => { + if (error.params?.[key]) { + error.params[key] = `'${error.params[key]}'`; + } + }); + if (error.params?.deps) { + error.params.deps = error.params.deps + .split(', ') + .map((v: string) => `'${v}'`) + .join(', '); } }); this.localizer(compiledValidator.errors); // Revert to originals (compiledValidator.errors ?? []).forEach((error) => { - if (error.params?.missingProperty) { - error.params.missingProperty = error.params.missingProperty.slice(1, -1); + ['missingProperty', 'property'].forEach((key) => { + if (error.params?.[key]) { + error.params[key] = error.params[key].slice(1, -1); + } + }); + if (error.params?.deps) { + error.params.deps = error.params.deps + .split(', ') + .map((v: string) => v.slice(1, -1)) + .join(', '); } }); } diff --git a/packages/validator-ajv8/test/validator.test.ts b/packages/validator-ajv8/test/validator.test.ts index 2ce40017a7..bdc4433447 100644 --- a/packages/validator-ajv8/test/validator.test.ts +++ b/packages/validator-ajv8/test/validator.test.ts @@ -9,6 +9,7 @@ import { UiSchema, ValidatorType, } from '@rjsf/utils'; +import localize from 'ajv-i18n'; import AJV8Validator from '../src/validator'; import { Localizer } from '../src'; @@ -2252,6 +2253,240 @@ describe('AJV8Validator', () => { }); }); }); + describe('validating dependencies', () => { + beforeAll(() => { + validator = new AJV8Validator({ AjvClass: Ajv2019 }, localize.en as Localizer); + }); + it('should return an error when a dependent is missing', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + }, + billingAddress: { + type: 'string', + }, + }, + dependentRequired: { + creditCard: ['billingAddress'], + }, + }; + const errors = validator.validateFormData({ creditCard: 1234567890 }, schema); + const errMessage = "must have property 'billingAddress' when property 'creditCard' is present"; + expect(errors.errors[0].message).toEqual(errMessage); + expect(errors.errors[0].stack).toEqual(errMessage); + expect(errors.errorSchema).toEqual({ + billingAddress: { + __errors: [errMessage], + }, + }); + expect(errors.errors[0].params.deps).toEqual('billingAddress'); + }); + it('should return an error when multiple dependents are missing', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + }, + holderName: { + type: 'string', + }, + billingAddress: { + type: 'string', + }, + }, + dependentRequired: { + creditCard: ['holderName', 'billingAddress'], + }, + }; + const errors = validator.validateFormData({ creditCard: 1234567890 }, schema); + const errMessage = "must have properties 'holderName', 'billingAddress' when property 'creditCard' is present"; + expect(errors.errors[0].message).toEqual(errMessage); + expect(errors.errors[0].stack).toEqual(errMessage); + expect(errors.errorSchema).toEqual({ + billingAddress: { + __errors: [errMessage], + }, + holderName: { + __errors: [errMessage], + }, + }); + expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress'); + }); + it('should return an error with title when a dependent is missing', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + title: 'Credit card', + }, + billingAddress: { + type: 'string', + title: 'Billing address', + }, + }, + dependentRequired: { + creditCard: ['billingAddress'], + }, + }; + const errors = validator.validateFormData({ creditCard: 1234567890 }, schema); + const errMessage = "must have property 'Billing address' when property 'Credit card' is present"; + expect(errors.errors[0].message).toEqual(errMessage); + expect(errors.errors[0].stack).toEqual(errMessage); + expect(errors.errorSchema).toEqual({ + billingAddress: { + __errors: [errMessage], + }, + }); + expect(errors.errors[0].params.deps).toEqual('billingAddress'); + }); + it('should return an error with titles when multiple dependents are missing', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + title: 'Credit card', + }, + holderName: { + type: 'string', + title: 'Holder name', + }, + billingAddress: { + type: 'string', + title: 'Billing address', + }, + }, + dependentRequired: { + creditCard: ['holderName', 'billingAddress'], + }, + }; + const errors = validator.validateFormData({ creditCard: 1234567890 }, schema); + const errMessage = + "must have properties 'Holder name', 'Billing address' when property 'Credit card' is present"; + expect(errors.errors[0].message).toEqual(errMessage); + expect(errors.errors[0].stack).toEqual(errMessage); + expect(errors.errorSchema).toEqual({ + billingAddress: { + __errors: [errMessage], + }, + holderName: { + __errors: [errMessage], + }, + }); + expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress'); + }); + it('should return an error with uiSchema title when a dependent is missing', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + }, + billingAddress: { + type: 'string', + }, + }, + dependentRequired: { + creditCard: ['billingAddress'], + }, + }; + const uiSchema: UiSchema = { + creditCard: { + 'ui:title': 'uiSchema Credit card', + }, + billingAddress: { + 'ui:title': 'uiSchema Billing address', + }, + }; + const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema); + const errMessage = + "must have property 'uiSchema Billing address' when property 'uiSchema Credit card' is present"; + expect(errors.errors[0].message).toEqual(errMessage); + expect(errors.errors[0].stack).toEqual(errMessage); + expect(errors.errorSchema).toEqual({ + billingAddress: { + __errors: [errMessage], + }, + }); + expect(errors.errors[0].params.deps).toEqual('billingAddress'); + }); + it('should return an error with uiSchema titles when multiple dependents are missing', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + }, + holderName: { + type: 'string', + }, + billingAddress: { + type: 'string', + }, + }, + dependentRequired: { + creditCard: ['holderName', 'billingAddress'], + }, + }; + const uiSchema: UiSchema = { + creditCard: { + 'ui:title': 'uiSchema Credit card', + }, + holderName: { + 'ui:title': 'uiSchema Holder name', + }, + billingAddress: { + 'ui:title': 'uiSchema Billing address', + }, + }; + const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema); + const errMessage = + "must have properties 'uiSchema Holder name', 'uiSchema Billing address' when property 'uiSchema Credit card' is present"; + expect(errors.errors[0].message).toEqual(errMessage); + expect(errors.errors[0].stack).toEqual(errMessage); + expect(errors.errorSchema).toEqual({ + billingAddress: { + __errors: [errMessage], + }, + holderName: { + __errors: [errMessage], + }, + }); + expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress'); + }); + it('should handle the case when errors are not present', () => { + schema = { + type: 'object', + properties: { + creditCard: { + type: 'number', + }, + holderName: { + type: 'string', + }, + billingAddress: { + type: 'string', + }, + }, + dependentRequired: { + creditCard: ['holderName', 'billingAddress'], + }, + }; + const errors = validator.validateFormData( + { + creditCard: 1234567890, + holderName: 'Alice', + billingAddress: 'El Camino Real', + }, + schema + ); + expect(errors.errors).toHaveLength(0); + }); + }); }); describe('validator.validateFormData(), custom options, localizer and Ajv2020', () => { let validator: AJV8Validator;