-
Notifications
You must be signed in to change notification settings - Fork 59
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
Moved schema-validator logic to form engine #107
Closed
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
6cb30fa
added form-validator logic
84121be
Schema-validator
604efb8
render control instead of "view" mode
e1cf8ba
Revert changes on the controls
e32fbdf
Relocated schema validator to validators dir
5dc978f
Merge branch 'main' into dev
arodidev e9558b5
renaming error and warning arrays
672c387
Update and overhaul
1c17c0d
Cleanup
b8dc67e
export validator hook
e5b6a0b
added config schema
3dc7968
Test production
f7ca7b0
Merge branch 'openmrs:main' into dev
arodidev bbf444f
Linting
arodidev 84ecf2e
revamp schema validator
arodidev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { Type } from '@openmrs/esm-framework'; | ||
|
||
export const configSchema = { | ||
dataTypeToRenderingMap: { | ||
_description: 'A map used to match concept datatypes to rendering types', | ||
_type: Type.Object, | ||
_default: { | ||
Numeric: ['number', 'fixed-value'], | ||
Coded: ['select', 'checkbox', 'radio', 'toggle', 'content-switcher', 'fixed-value'], | ||
Text: ['text', 'textarea', 'fixed-value'], | ||
Date: ['date', 'fixed-value'], | ||
Datetime: ['datetime', 'fixed-value'], | ||
Boolean: ['toggle', 'select', 'radio', 'content-switcher', 'fixed-value'], | ||
Rule: ['repeating', 'group'], | ||
}, | ||
}, | ||
|
||
conceptDataTypes: { | ||
_description: 'An object containing availabe datatypes', | ||
_type: Type.Object, | ||
_default: { | ||
Numeric: 'Numeric', | ||
Coded: 'Coded', | ||
Text: 'Text', | ||
Date: 'Date', | ||
Datetime: 'Datetime', | ||
Boolean: 'Boolean', | ||
Rule: 'Rule', | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import useSWRImmutable from 'swr/immutable'; | ||
import { openmrsFetch, OpenmrsResource } from '@openmrs/esm-framework'; | ||
|
||
const conceptRepresentation = | ||
'custom:(uuid,display,datatype,conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))'; | ||
|
||
export function useDatatype(references: Set<string>) { | ||
// TODO: handle paging (ie when number of concepts greater than default limit per page) | ||
const { data, error, isLoading } = useSWRImmutable<{ data: { results: Array<OpenmrsResource> } }, Error>( | ||
`/ws/rest/v1/concept?references=${Array.from(references).join(',')}&v=${conceptRepresentation}`, | ||
openmrsFetch, | ||
); | ||
|
||
const filteredSet = Array.from(references).filter( | ||
eachItem => !data?.data.results.some(item => eachItem === item.uuid), | ||
); | ||
|
||
return { concepts: data?.data.results, error, isLoading, filteredSet }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { useDatatype } from './useDatatype'; | ||
import { useEffect, useMemo, useState } from 'react'; | ||
import { useConfig } from '@openmrs/esm-framework'; | ||
import { ConceptTrue, ConceptFalse } from '../constants'; | ||
import { OHRIFormField, OHRIFormSchema } from '../api/types'; | ||
|
||
export function useSchemaValidator(schema: OHRIFormSchema, doValidate: Boolean) { | ||
const [errors, setErrors] = useState([]); | ||
const [warnings, setWarnings] = useState([]); | ||
const [questionFields, setQuestionFields] = useState<Array<OHRIFormField>>([]); | ||
const [answerFields, setAnswerFields] = useState([]); | ||
const [isValidating, setIsValidating] = useState(true); | ||
const [conceptSet, setConceptSet] = useState<Set<string>>(); | ||
const [answerConceptSet, setAnswerConceptSet] = useState<Set<string>>(); | ||
const { dataTypeToRenderingMap, conceptDataTypes } = useConfig({ | ||
// externalModuleName: '@openmrs/openmrs-form-engine-lib', | ||
}); | ||
|
||
if (!schema) { | ||
throw new Error('Form schema is not provided'); | ||
} | ||
|
||
useMemo(() => { | ||
let questionSearchReferences = []; | ||
let nullSearchReferenceErrors = []; | ||
|
||
schema.pages?.forEach((page) => | ||
page.sections?.forEach((section) => | ||
section.questions?.forEach((question) => { | ||
if (question.type === 'obsGroup') { | ||
question.questions.forEach((obsGrpQuestion) => { | ||
const fieldConceptReference = extractQuestionSearchReferenceFromField(obsGrpQuestion); | ||
if (fieldConceptReference) questionSearchReferences.push(fieldConceptReference); | ||
else { | ||
nullSearchReferenceErrors.push[`${obsGrpQuestion.id} has no search reference`]; | ||
} | ||
}); | ||
} else { | ||
const fieldConceptReference = extractQuestionSearchReferenceFromField(question); | ||
if (fieldConceptReference) { | ||
questionSearchReferences.push(fieldConceptReference); | ||
} | ||
} | ||
}), | ||
), | ||
); | ||
setConceptSet(new Set(questionSearchReferences)); | ||
setErrors((previousErrors) => ({ | ||
...previousErrors, | ||
nullSearchReferenceErrors: nullSearchReferenceErrors, | ||
})); | ||
}, [schema]); | ||
|
||
const findUnresolvedConcepts = (questionFields, filteredSetArray) => { | ||
const unresolvedConcepts = questionFields | ||
?.filter((questionFieldsItem) => { | ||
return filteredSetArray?.includes(questionFieldsItem.questionOptions.concept); | ||
}) | ||
?.map((item) => { | ||
return { | ||
errorMessage: `❓ Field Concept "${item.questionOptions.concept}" not found`, | ||
field: item, | ||
}; | ||
}); | ||
|
||
setErrors((prevState) => [...prevState, ...unresolvedConcepts]); | ||
}; | ||
|
||
const unresolvedAnswers = (questionFields, filteredSetArray) => { | ||
const unresolvedConcepts = questionFields | ||
?.filter((questionFieldsItem) => { | ||
return filteredSetArray?.includes(questionFieldsItem.concept); | ||
}) | ||
?.map((item) => { | ||
return { | ||
errorMessage: `Answer Concept "${item.concept}" not found`, | ||
field: item, | ||
}; | ||
}); | ||
|
||
setErrors((prevState) => [...prevState, ...unresolvedConcepts]); | ||
}; | ||
|
||
const dataTypeChecker = (responseObject, questionFields) => { | ||
questionFields | ||
?.filter((item) => item.questionOptions.concept === responseObject.uuid) | ||
.map((item) => { | ||
responseObject.datatype.name === conceptDataTypes.Boolean && | ||
item.questionOptions.answers.forEach((answer) => { | ||
if (![ConceptTrue, ConceptFalse].includes(answer.concept)) { | ||
setErrors((prevErrors) => [ | ||
...prevErrors, | ||
{ | ||
errorMessage: `❌ concept "${item.questionOptions.concept}" of type "boolean" has a non-boolean answer "${answer.label}"`, | ||
field: item, | ||
}, | ||
]); | ||
} | ||
}); | ||
|
||
responseObject.datatype.name === conceptDataTypes.Coded && | ||
item.questionOptions.answers.forEach((answer) => { | ||
if (!responseObject.answers?.some((answerObject) => answerObject.uuid === answer.concept)) { | ||
setWarnings((prevWarnings) => [ | ||
...prevWarnings, | ||
{ | ||
warningMessage: `⚠️ answer: "${answer.label}" - "${answer.concept}" does not exist in the response answers but exists in the form`, | ||
field: item, | ||
}, | ||
]); | ||
} | ||
}); | ||
|
||
dataTypeToRenderingMap.hasOwnProperty(responseObject?.datatype?.name) && | ||
!dataTypeToRenderingMap[responseObject.datatype.name].includes(item.questionOptions.rendering) && | ||
setErrors((prevErrors) => [ | ||
...prevErrors, | ||
{ | ||
errorMessage: `❌ ${item.questionOptions.concept}: datatype "${responseObject.datatype.display}" doesn't match control type "${item.questionOptions.rendering}"`, | ||
field: item, | ||
}, | ||
]); | ||
|
||
!dataTypeToRenderingMap.hasOwnProperty(responseObject.datatype.name) && | ||
setErrors((prevErrors) => [ | ||
...prevErrors, | ||
{ errorMessage: `Untracked datatype "${responseObject.datatype.display}"`, field: item }, | ||
]); | ||
}); | ||
}; | ||
|
||
function extractQuestionSearchReferenceFromField(formField) { | ||
setQuestionFields((prevArray) => [...prevArray, formField]); | ||
const searchRef = formField.questionOptions.concept | ||
? formField.questionOptions.concept | ||
: formField.questionOptions.conceptMappings?.length | ||
? formField.questionOptions.conceptMappings | ||
?.map((mapping) => { | ||
return `${mapping.type}:${mapping.value}`; | ||
}) | ||
.join(',') | ||
: ''; | ||
|
||
return searchRef; | ||
} | ||
|
||
const { concepts, filteredSet, isLoading } = useDatatype(conceptSet); | ||
const { | ||
concepts: answerConcepts, | ||
filteredSet: filteredSetAnswers, | ||
isLoading: isLoadingAnswers, | ||
} = useDatatype(answerConceptSet); | ||
|
||
useEffect(() => { | ||
if (concepts?.length) { | ||
findUnresolvedConcepts(questionFields, filteredSet); | ||
concepts?.forEach((concept) => { | ||
dataTypeChecker(concept, questionFields); | ||
}); | ||
setIsValidating((prevValue) => !prevValue); | ||
} | ||
}, [concepts]); | ||
|
||
useEffect(() => { | ||
if (answerConcepts?.length) { | ||
unresolvedAnswers(answerFields, filteredSetAnswers); | ||
} | ||
}, [answerConcepts]); | ||
|
||
if (!doValidate) { | ||
return { errors: [], warnings: [], isValidating }; | ||
} | ||
|
||
return { errors, warnings, isValidating }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Assuming this returns concepts, what stops you from reusing the
useConcepts()
hook?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 think we had this conversation a while back to create a new custom hook that also returns a filtered set with unresolved concepts, but if need be I can revert back to using the useConcept hook and modifying it.