diff --git a/package.json b/package.json index 42e28c7..3790ac9 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "@carbon/react": "^1.38.0", "@openmrs/openmrs-form-engine-lib": "next", + "ajv": "^8.12.0", "dotenv": "^16.3.1", "file-loader": "^6.2.0", "fuzzy": "^0.1.3", diff --git a/src/components/form-editor/form-editor.component.tsx b/src/components/form-editor/form-editor.component.tsx index 1b7a7fa..81d997a 100644 --- a/src/components/form-editor/form-editor.component.tsx +++ b/src/components/form-editor/form-editor.component.tsx @@ -64,6 +64,8 @@ const FormEditor: React.FC = () => { const [status, setStatus] = useState('idle'); const [isMaximized, setIsMaximized] = useState(false); const [stringifiedSchema, setStringifiedSchema] = useState(schema ? JSON.stringify(schema, null, 2) : ''); + const [schemaValidationErrors, setSchemaValidationErrors] = useState>([]); + const [validationOn, setValidationOn] = useState(false); const isLoadingFormOrSchema = Boolean(formUuid) && (isLoadingClobdata || isLoadingForm); @@ -229,6 +231,30 @@ const FormEditor: React.FC = () => { ); }; + const SchemaValidationErrorWrapper = () => { + return ( +
+
+

+ {t('schemaErrorTitle', 'Validation failed with {{errorsCount}} {{plural}}', { + errorsCount: schemaValidationErrors.length, + plural: schemaValidationErrors.length === 1 ? 'error' : 'errors', + })} +

+ + Reference schema + +
+ {schemaValidationErrors.map((error, index) => ( +
+
{error.err}
+
+
{error.msg}
+
+ ))} +
+ ); + }; const downloadableSchema = useMemo( () => new Blob([JSON.stringify(schema, null, 2)], { @@ -259,30 +285,47 @@ const FormEditor: React.FC = () => { })} > -
- {isLoadingFormOrSchema ? ( - - ) : ( -

{form?.name}

- )} - -
- {isNewSchema && !schema ? ( - - ) : null} - - -
+
+ {schemaValidationErrors.length > 0 && validationOn ? ( + + ) : schemaValidationErrors.length === 0 && validationOn ? ( +
+ {t('successSchemaValidationMessage', 'No errors found in the JSON schema')} +
+ ) : null}
{t('schemaEditor', 'Schema editor')} +
+ {isLoadingFormOrSchema ? ( + + ) : ( +

{form?.name}

+ )} +
+ {schema ? ( + + ) : null} + {isNewSchema && !schema ? ( + + ) : null} + + +
+
{schema ? ( - <> +
) : null}
{formError ? ( @@ -325,7 +368,7 @@ const FormEditor: React.FC = () => { ) : null}
) => void; onSchemaChange: (stringifiedSchema: string) => void; stringifiedSchema: string; } -const SchemaEditor: React.FC = ({ invalidJsonErrorMessage, onSchemaChange, stringifiedSchema }) => { - const { t } = useTranslation(); +const SchemaEditor: React.FC = ({ + onSchemaChange, + stringifiedSchema, + setSchemaValidationErrors, +}) => { + const [ajv, setAjv] = useState({ validate: null }); + + useEffect(() => { + const fetchSchema = async () => { + try { + const response = await fetch('https://json.openmrs.org/form.schema.json'); + const schema = await response.json(); + + // Compile the JSON schema using Ajv + const validator = new Ajv({ allErrors: true }); + const validate = validator.compile(schema); + setAjv({ validate }); + } catch (error) { + console.error('Error fetching JSON schema:', error); + } + }; + + void fetchSchema(); + }, []); + + const handleSchemaChange = (newSchema: string) => { + if (ajv.validate) { + try { + const newSchemaJson = JSON.parse(newSchema); + const isValid = ajv.validate(newSchemaJson); + if (!isValid) { + const errorMessages: Array<{ err: string; msg: string }> = ajv.validate.errors.map((error: AjvError) => { + if (error.keyword === 'type') { + return { err: 'Invalid Type', msg: `${error.instancePath.substring(1)} ${error.message}` }; + } + const paramsKey = Object.keys(error.params)[0]; + const message = error.message.charAt(0).toUpperCase() + error.message.slice(1); + return { err: paramsKey, msg: message }; + }); + setSchemaValidationErrors(errorMessages); + } else { + setSchemaValidationErrors([]); + } + } catch (error) { + setSchemaValidationErrors([{ err: 'Invalid JSON', msg: 'Parse error, invalid JSON format' }]); + } + } + onSchemaChange(newSchema); + }; return ( - <> - {invalidJsonErrorMessage ? ( -
-

{t('schemaError', "There's an error in your schema.")}

-

{invalidJsonErrorMessage}

-
- ) : null} - - - + ); }; diff --git a/src/components/schema-editor/schema-editor.scss b/src/components/schema-editor/schema-editor.scss index e3d4aa1..544ecdf 100644 --- a/src/components/schema-editor/schema-editor.scss +++ b/src/components/schema-editor/schema-editor.scss @@ -12,14 +12,6 @@ } } -.errorContainer { - @include type.type-style("body-compact-02"); - background-color: colors.$red-20; - color: colors.$red-70; - padding: 1.5rem; - margin: 1rem 0; -} - .heading { @include type.type-style('heading-compact-02'); margin-bottom: 1rem; diff --git a/translations/en.json b/translations/en.json index a85e707..d960afb 100644 --- a/translations/en.json +++ b/translations/en.json @@ -161,7 +161,7 @@ "saving": "Saving", "schemaActions": "Schema actions", "schemaEditor": "Schema editor", - "schemaError": "There's an error in your schema.", + "schemaErrorTitle": "Validation failed with {{errorsCount}} {{plural}}", "schemaLoadError": "Error loading schema", "schemaNotFound": "Schema not found", "schemaNotFoundText": "The schema originally associated with this form could not be found. A draft schema was found saved in your browser's local storage. Would you like to load it instead?", @@ -180,6 +180,7 @@ "source": "Source", "startBuilding": "Start building", "success": "Success!", + "successSchemaValidationMessage": "No errors found in the JSON schema", "timerOnly": "Timer only", "tryAgain": "Try again", "typeRequired": "Type is required", @@ -190,6 +191,7 @@ "unpublishForm": "Unpublish Form", "unpublishing": "Unpublishing", "updateExistingForm": "Update existing version", + "validateSchema": "Validate schema", "validFieldTypeRequired": "A valid field type value is required", "validRenderingTypeRequired": "A valid rendering type value is required", "version": "Version", diff --git a/yarn.lock b/yarn.lock index f4b0e20..0a1e90b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3465,6 +3465,7 @@ __metadata: "@types/webpack-env": "npm:^1.18.1" "@typescript-eslint/eslint-plugin": "npm:^7.5.0" "@typescript-eslint/parser": "npm:^6.7.0" + ajv: "npm:^8.12.0" css-loader: "npm:^6.8.1" dotenv: "npm:^16.3.1" eslint: "npm:^8.49.0" @@ -6340,6 +6341,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.12.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + uri-js: "npm:^4.2.2" + checksum: 10/b406f3b79b5756ac53bfe2c20852471b08e122bc1ee4cde08ae4d6a800574d9cd78d60c81c69c63ff81e4da7cd0b638fafbb2303ae580d49cf1600b9059efb85 + languageName: node + linkType: hard + "ansi-escapes@npm:^4.2.1": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2"