Skip to content
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

(feat): Link the Form Builder to the standard schema and validate the schema Editor. #256

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 64 additions & 21 deletions src/components/form-editor/form-editor.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const FormEditor: React.FC = () => {
const [status, setStatus] = useState<Status>('idle');
const [isMaximized, setIsMaximized] = useState(false);
const [stringifiedSchema, setStringifiedSchema] = useState(schema ? JSON.stringify(schema, null, 2) : '');
const [schemaValidationErrors, setSchemaValidationErrors] = useState<Array<{ err: string; msg: string }>>([]);
const [validationOn, setValidationOn] = useState(false);

const isLoadingFormOrSchema = Boolean(formUuid) && (isLoadingClobdata || isLoadingForm);

Expand Down Expand Up @@ -229,6 +231,30 @@ const FormEditor: React.FC = () => {
);
};

const SchemaValidationErrorWrapper = () => {
return (
<div className={styles.errorContainer}>
<div className={styles.errorHeader}>
<p className={styles.errorHeading}>
{t('schemaErrorTitle', 'Validation failed with {{errorsCount}} {{plural}}', {
errorsCount: schemaValidationErrors.length,
plural: schemaValidationErrors.length === 1 ? 'error' : 'errors',
})}
</p>
<a href="https://json.openmrs.org/form.schema.json" target="_blank">
Reference schema
</a>
</div>
{schemaValidationErrors.map((error, index) => (
<div className={styles.errorMessage} key={index}>
<div className={styles.errKey}>{error.err}</div>
<div className={styles.dashedLine} />
<div className={styles.errDescription}>{error.msg}</div>
</div>
))}
</div>
);
};
const downloadableSchema = useMemo(
() =>
new Blob([JSON.stringify(schema, null, 2)], {
Expand Down Expand Up @@ -259,30 +285,47 @@ const FormEditor: React.FC = () => {
})}
>
<Column lg={responsiveSize} md={responsiveSize} className={styles.column}>
<div className={styles.actionButtons}>
{isLoadingFormOrSchema ? (
<InlineLoading description={t('loadingSchema', 'Loading schema') + '...'} />
) : (
<h1 className={styles.formName}>{form?.name}</h1>
)}

<div>
{isNewSchema && !schema ? (
<Button kind="ghost" onClick={inputDummySchema}>
{t('inputDummySchema', 'Input dummy schema')}
</Button>
) : null}

<Button kind="ghost" onClick={renderSchemaChanges}>
<span>{t('renderChanges', 'Render changes')}</span>
</Button>
</div>
<div className={styles.errorSection}>
{schemaValidationErrors.length > 0 && validationOn ? (
<SchemaValidationErrorWrapper />
) : schemaValidationErrors.length === 0 && validationOn ? (
<div className={styles.successMessage}>
{t('successSchemaValidationMessage', 'No errors found in the JSON schema')}
</div>
) : null}
</div>
<div>
<div className={styles.heading}>
<span className={styles.tabHeading}>{t('schemaEditor', 'Schema editor')}</span>
<div className={styles.actionButtons}>
{isLoadingFormOrSchema ? (
<InlineLoading description={t('loadingSchema', 'Loading schema') + '...'} />
) : (
<h1 className={styles.formName}>{form?.name}</h1>
)}
<div>
{schema ? (
<Button kind="ghost" onClick={() => setValidationOn(true)}>
{t('validateSchema', 'Validate schema')}
</Button>
) : null}
{isNewSchema && !schema ? (
<Button kind="ghost" onClick={inputDummySchema}>
{t('inputDummySchema', 'Input dummy schema')}
</Button>
) : null}

<Button
kind="ghost"
disabled={schemaValidationErrors.length || invalidJsonErrorMessage}
onClick={renderSchemaChanges}
>
<span>{t('renderChanges', 'Render changes')}</span>
</Button>
</div>
</div>
{schema ? (
<>
<div className={styles.formActionBtns}>
<Button
enterDelayMs={300}
renderIcon={isMaximized ? Minimize : Maximize}
Expand Down Expand Up @@ -314,7 +357,7 @@ const FormEditor: React.FC = () => {
tooltipAlignment="start"
/>
</a>
</>
</div>
) : null}
</div>
{formError ? (
Expand All @@ -325,7 +368,7 @@ const FormEditor: React.FC = () => {
) : null}
<div className={styles.editorContainer}>
<SchemaEditor
invalidJsonErrorMessage={invalidJsonErrorMessage}
setSchemaValidationErrors={setSchemaValidationErrors}
isLoading={isLoadingFormOrSchema}
onSchemaChange={handleSchemaChange}
stringifiedSchema={stringifiedSchema}
Expand Down
64 changes: 61 additions & 3 deletions src/components/form-editor/form-editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@use "@carbon/type";

.container {
padding: 2rem;
padding: 0rem 2rem;
display: flex;
flex-direction: column;
}
Expand Down Expand Up @@ -40,7 +40,6 @@
display: flex;
align-items: center;
justify-content: space-between;
margin: 1rem 0;

button {
margin-left: 1rem
Expand All @@ -59,14 +58,15 @@
display: flex;
margin-right: 1rem;
align-items: center;
justify-content: space-between;
width: 98%;
}

.tabHeading {
display: flex;
align-items: center;
@include type.type-style('heading-compact-01');
min-height: 2.5rem;
width: 100%;
padding: 0.75rem;
}

Expand Down Expand Up @@ -97,3 +97,61 @@
button {
padding-block-start: 0.5rem;
}

.errorContainer {
@include type.type-style("body-compact-02");
background-color: #fee2e2;
color: #b91c1c;
padding: 1rem;
margin: 0.5rem 0;
display: flex;
flex-direction: column;
width: 100%;
max-height: 7rem;
overflow-y: auto;
}

.errorHeader {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 1rem;
}

.errorHeading {
font-weight: bold;
}

.errKey {
font-weight: bold;
text-transform: capitalize;
}
.errorMessage {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}

.successMessage {
padding: 1rem;
background-color: #dcfce7;
color: #15803d;
}
.formActionBtns {
display: flex;
align-items: center;
gap: 0.5rem;
}
.errorSection {
width: 98%;
min-height: 5rem;
margin-bottom: 0.5rem;
}

.dashedLine {
border-bottom: 1px dashed #b91c1c; /* Adjust thickness and color as needed */
width: 100%; /* Set the width of the dashed line */
}
114 changes: 79 additions & 35 deletions src/components/schema-editor/schema-editor.component.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,94 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import AceEditor from 'react-ace';
import Ajv from 'ajv';
import 'ace-builds/webpack-resolver';
import 'ace-builds/src-noconflict/ext-language_tools';
import { useTranslation } from 'react-i18next';
import styles from './schema-editor.scss';

interface AjvError {
keyword: string;
schemaPath: string;
instancePath: string;
message: string;
params: { key: string; value: string };
}
interface SchemaEditorProps {
isLoading: boolean;
invalidJsonErrorMessage: string;
setSchemaValidationErrors: (errors: Array<{ err: string; msg: string }>) => void;
onSchemaChange: (stringifiedSchema: string) => void;
stringifiedSchema: string;
}

const SchemaEditor: React.FC<SchemaEditorProps> = ({ invalidJsonErrorMessage, onSchemaChange, stringifiedSchema }) => {
const { t } = useTranslation();
const SchemaEditor: React.FC<SchemaEditorProps> = ({
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 ? (
<div className={styles.errorContainer}>
<p className={styles.heading}>{t('schemaError', "There's an error in your schema.")}</p>
<p>{invalidJsonErrorMessage}</p>
</div>
) : null}

<AceEditor
style={{ height: '100vh', width: '100%' }}
mode="json"
theme="textmate"
name="schemaEditor"
onChange={onSchemaChange}
fontSize={15}
showPrintMargin={false}
showGutter={true}
highlightActiveLine={true}
value={stringifiedSchema}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
displayIndentGuides: true,
enableSnippets: false,
showLineNumbers: true,
tabSize: 2,
}}
/>
</>
<AceEditor
style={{ height: '100vh', width: '100%' }}
mode="json"
theme="textmate"
name="schemaEditor"
onChange={handleSchemaChange}
fontSize={15}
showPrintMargin={false}
showGutter={true}
highlightActiveLine={true}
value={stringifiedSchema}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
displayIndentGuides: true,
enableSnippets: true,
showLineNumbers: true,
tabSize: 2,
}}
/>
);
};

Expand Down
8 changes: 0 additions & 8 deletions src/components/schema-editor/schema-editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
Loading
Loading