From 69cd463f774105e3d7a92e4240d44a94dba2294d Mon Sep 17 00:00:00 2001 From: Elena Grednikova Date: Fri, 15 Nov 2024 18:49:02 +0100 Subject: [PATCH] Add support of cqf expressions for _text, _reaOnly and _required --- sdc-qrf/src/components.tsx | 187 ++++++++++++++++++++--------- sdc-qrf/src/utils.ts | 2 +- shared/src/contrib/aidbox/index.ts | 12 ++ 3 files changed, 144 insertions(+), 57 deletions(-) diff --git a/sdc-qrf/src/components.tsx b/sdc-qrf/src/components.tsx index 2a3c988..489611e 100644 --- a/sdc-qrf/src/components.tsx +++ b/sdc-qrf/src/components.tsx @@ -1,7 +1,8 @@ +import { Expression } from 'fhir/r4b'; import fhirpath from 'fhirpath'; import _ from 'lodash'; import isEqual from 'lodash/isEqual'; -import React, { ReactChild, useEffect, useContext, useMemo, useRef } from 'react'; +import React, { ReactChild, useEffect, useContext, useMemo, useRef, useState } from 'react'; import { QuestionnaireItem } from 'shared/src/contrib/aidbox'; @@ -58,8 +59,10 @@ export function QuestionItems(props: QuestionItemsProps) { ); } +const cqfExpressionExtensionUrl = 'http://hl7.org/fhir/StructureDefinition/cqf-expression'; + export function QuestionItem(props: QuestionItemProps) { - const { questionItem, context: initialContext, parentPath } = props; + const { questionItem: initialQuestionItem, context: initialContext, parentPath } = props; const { questionItemComponents, customWidgets, @@ -68,9 +71,20 @@ export function QuestionItem(props: QuestionItemProps) { itemControlGroupItemComponents, } = useContext(QRFContext); const { formValues, setFormValues } = useQuestionnaireResponseFormContext(); + const [questionItem, setQuestionItem] = useState(initialQuestionItem); + const prevQuestionItem: QuestionnaireItem | undefined = usePreviousValue(questionItem); - const { type, linkId, calculatedExpression, variable, repeats, itemControl, _text } = - questionItem; + const { + type, + linkId, + calculatedExpression, + variable, + repeats, + itemControl, + _text, + _readOnly, + _required, + } = questionItem; const fieldPath = useMemo(() => [...parentPath, linkId!], [parentPath, linkId]); // TODO: how to do when item is not in QR (e.g. default element of repeatable group) @@ -89,57 +103,37 @@ export function QuestionItem(props: QuestionItemProps) { _.get(formValues, fieldPath), ); + const itemContext = useMemo( + () => (isGroupItem(questionItem, context) ? context[0] : context), + [questionItem, context], + ); + useEffect(() => { - if (!isGroupItem(questionItem, context) && calculatedExpression) { - // TODO: Add support for x-fhir-query - if (calculatedExpression.language === 'text/fhirpath') { - try { - const newValues = fhirpath.evaluate( - context.context || {}, - calculatedExpression.expression!, - context as ItemContext, - ); - const newAnswers: FormAnswerItems[] | undefined = newValues.length - ? repeats - ? newValues.map((answer: any) => ({ - value: wrapAnswerValue(type, answer), - })) - : [{ value: wrapAnswerValue(type, newValues[0]) }] - : undefined; - - if ( - !isEqual( - newAnswers?.map((answer) => answer.value), - prevAnswers?.map((answer) => answer.value), - ) - ) { - const allValues = _.set(_.cloneDeep(formValues), fieldPath, newAnswers); - setFormValues(allValues, fieldPath, newAnswers); - } - } catch (err: unknown) { - throw Error( - `FHIRPath expression evaluation failure for "calculatedExpression" in ${questionItem.linkId}: ${err}`, - ); - } - } - } - if (!isGroupItem(questionItem, context) && _text) { - try { - const extension = findExtensionByUrl( - 'http://hl7.org/fhir/StructureDefinition/cqf-expression', - _text.extension, - ); - if (_text && extension?.valueExpression?.expression) { - questionItem.text = fhirpath.evaluate( - context.context || {}, - extension.valueExpression.expression, - context as ItemContext, - )[0]; - } - } catch (err: unknown) { - throw Error( - `FHIRPath expression evaluation failure for "cqfExpression" in ${questionItem.linkId}: ${err}`, - ); + // TODO: think about use cases for group context + if (itemContext && calculatedExpression) { + const newValues = evaluateQuestionItemExpression( + linkId, + 'calculatedExpression', + itemContext, + calculatedExpression, + ); + + const newAnswers: FormAnswerItems[] | undefined = newValues.length + ? repeats + ? newValues.map((answer: any) => ({ + value: wrapAnswerValue(type, answer), + })) + : [{ value: wrapAnswerValue(type, newValues[0]) }] + : undefined; + + if ( + !isEqual( + newAnswers?.map((answer) => answer.value), + prevAnswers?.map((answer) => answer.value), + ) + ) { + const allValues = _.set(_.cloneDeep(formValues), fieldPath, newAnswers); + setFormValues(allValues, fieldPath, newAnswers); } } }, [ @@ -150,12 +144,71 @@ export function QuestionItem(props: QuestionItemProps) { parentPath, repeats, type, - questionItem, + linkId, + itemContext, prevAnswers, fieldPath, - _text, ]); + useEffect(() => { + if (itemContext && _text) { + const extension = findExtensionByUrl(cqfExpressionExtensionUrl, _text.extension); + const cqfExpression = extension?.valueExpression; + const calculatedValue = + evaluateQuestionItemExpression( + linkId, + '_text.cqfExpression', + itemContext, + cqfExpression, + ) ?? initialQuestionItem.text; + + if (prevQuestionItem?.text !== calculatedValue) { + setQuestionItem((qi) => ({ + ...qi, + text: calculatedValue, + })); + } + } + + if (itemContext && _readOnly) { + const extension = findExtensionByUrl(cqfExpressionExtensionUrl, _readOnly.extension); + const cqfExpression = extension?.valueExpression; + const calculatedValue = + evaluateQuestionItemExpression( + linkId, + '_readOnly.cqfExpression', + itemContext, + cqfExpression, + ) ?? initialQuestionItem.readOnly; + + if (prevQuestionItem?.readOnly !== calculatedValue) { + setQuestionItem((qi) => ({ + ...qi, + readOnly: calculatedValue, + })); + } + } + + if (itemContext && _required) { + const extension = findExtensionByUrl(cqfExpressionExtensionUrl, _required.extension); + const cqfExpression = extension?.valueExpression; + const calculatedValue = + evaluateQuestionItemExpression( + linkId, + '_required.cqfExpression', + itemContext, + cqfExpression, + ) ?? initialQuestionItem.required; + + if (prevQuestionItem?.required !== calculatedValue) { + setQuestionItem((qi) => ({ + ...qi, + required: calculatedValue, + })); + } + } + }, [linkId, initialQuestionItem, prevQuestionItem, itemContext, _text, _readOnly, _required]); + if (isGroupItem(questionItem, context)) { if (itemControl) { if ( @@ -264,3 +317,25 @@ function isGroupItem( ): context is ItemContext[] { return questionItem.type === 'group'; } + +export function evaluateQuestionItemExpression( + linkId: string, + path: string, + context: ItemContext, + expression?: Expression, +) { + if (!expression) { + return; + } + + if (expression.language !== 'text/fhirpath') { + console.error('Only fhirpath expression is supported'); + return; + } + + try { + return fhirpath.evaluate(context.context ?? {}, expression.expression!, context)[0]; + } catch (err: unknown) { + throw Error(`FHIRPath expression evaluation failure for ${linkId}.${path}: ${err}`); + } +} diff --git a/sdc-qrf/src/utils.ts b/sdc-qrf/src/utils.ts index 1b050c9..bd9cfd7 100644 --- a/sdc-qrf/src/utils.ts +++ b/sdc-qrf/src/utils.ts @@ -528,7 +528,7 @@ function isQuestionEnabled(args: IsQuestionEnabledArgs) { return expressionResult; } catch (err: unknown) { throw Error( - `FHIRPath expression evaluation failure for "enableWhenExpression" in ${args.qItem.linkId}: ${err}`, + `FHIRPath expression evaluation failure for ${args.qItem.linkId}.enableWhenExpression: ${err}`, ); } } diff --git a/shared/src/contrib/aidbox/index.ts b/shared/src/contrib/aidbox/index.ts index be95b49..8b5adeb 100644 --- a/shared/src/contrib/aidbox/index.ts +++ b/shared/src/contrib/aidbox/index.ts @@ -14409,6 +14409,7 @@ export interface QuestionnaireItem { prefix?: string; /** Don't allow human editing */ readOnly?: boolean; + _readOnly?: QuestionnaireItemReadOnly; /** NOTE: from extension http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource */ /** Where the type for a question is Reference, indicates a type of resource that is permitted. */ referenceResource?: code[]; @@ -14416,6 +14417,7 @@ export interface QuestionnaireItem { repeats?: boolean; /** Whether the item must be included in data results */ required?: boolean; + _required?: QuestionnaireItemRequired; /** NOTE: from extension https://jira.hl7.org/browse/FHIR-22356#subQuestionnaire */ subQuestionnaire?: canonical; /** Primary text for the item */ @@ -14463,6 +14465,16 @@ export interface QuestionnaireItemText { extension?: Extension[]; } +export interface QuestionnaireItemReadOnly { + cqfExpression?: Expression; + extension?: Extension[]; +} + +export interface QuestionnaireItemRequired { + cqfExpression?: Expression; + extension?: Extension[]; +} + export interface QuestionnaireItemAnswerOption { /** Additional content defined by implementations */ extension?: Extension[];