diff --git a/apps/smart-forms-app/package.json b/apps/smart-forms-app/package.json index 090f8adc8..d17e93fb1 100644 --- a/apps/smart-forms-app/package.json +++ b/apps/smart-forms-app/package.json @@ -28,7 +28,7 @@ "dependencies": { "@aehrc/sdc-assemble": "^1.2.0", "@aehrc/sdc-populate": "^2.2.1", - "@aehrc/smart-forms-renderer": "^0.35.4", + "@aehrc/smart-forms-renderer": "^0.35.5", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.16", diff --git a/documentation/package.json b/documentation/package.json index 0460c5c73..ab31e167b 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -15,7 +15,7 @@ "typecheck": "tsc" }, "dependencies": { - "@aehrc/smart-forms-renderer": "^0.35.4", + "@aehrc/smart-forms-renderer": "^0.35.5", "@docusaurus/core": "^3.4.0", "@docusaurus/preset-classic": "^3.4.0", "@docusaurus/theme-live-codeblock": "^3.4.0", diff --git a/package-lock.json b/package-lock.json index 2df744778..dd73fb66b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "dependencies": { "@aehrc/sdc-assemble": "^1.2.0", "@aehrc/sdc-populate": "^2.2.1", - "@aehrc/smart-forms-renderer": "^0.35.4", + "@aehrc/smart-forms-renderer": "^0.35.5", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.16", @@ -465,7 +465,7 @@ "name": "@aehrc/smart-forms-documentation", "version": "0.0.0", "dependencies": { - "@aehrc/smart-forms-renderer": "^0.35.4", + "@aehrc/smart-forms-renderer": "^0.35.5", "@docusaurus/core": "^3.4.0", "@docusaurus/preset-classic": "^3.4.0", "@docusaurus/theme-live-codeblock": "^3.4.0", @@ -43482,7 +43482,7 @@ }, "packages/smart-forms-renderer": { "name": "@aehrc/smart-forms-renderer", - "version": "0.35.4", + "version": "0.35.5", "license": "Apache-2.0", "dependencies": { "@aehrc/sdc-populate": "^2.2.1", diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index 5855734ef..a6e4358e6 100644 --- a/packages/smart-forms-renderer/package.json +++ b/packages/smart-forms-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@aehrc/smart-forms-renderer", - "version": "0.35.4", + "version": "0.35.5", "description": "FHIR Structured Data Captured (SDC) rendering engine for Smart Forms", "main": "lib/index.js", "scripts": { diff --git a/packages/smart-forms-renderer/src/interfaces/questionnaireStore.interface.ts b/packages/smart-forms-renderer/src/interfaces/questionnaireStore.interface.ts index 551aebc9a..4558f31e6 100644 --- a/packages/smart-forms-renderer/src/interfaces/questionnaireStore.interface.ts +++ b/packages/smart-forms-renderer/src/interfaces/questionnaireStore.interface.ts @@ -21,7 +21,7 @@ import type { LaunchContext } from './populate.interface'; import type { EnableWhenExpressions, EnableWhenItems } from './enableWhen.interface'; import type { CalculatedExpression } from './calculatedExpression.interface'; import type { AnswerExpression } from './answerExpression.interface'; -import type { Coding } from 'fhir/r4'; +import type { Coding, QuestionnaireItemAnswerOption } from 'fhir/r4'; export interface QuestionnaireModel { itemTypes: Record; @@ -32,6 +32,7 @@ export interface QuestionnaireModel { enableWhenExpressions: EnableWhenExpressions; calculatedExpressions: Record; answerExpressions: Record; + answerOptions: Record; processedValueSetCodings: Record; processedValueSetUrls: Record; fhirPathContext: Record; diff --git a/packages/smart-forms-renderer/src/stores/questionnaireStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireStore.ts index 3a139952f..2e2fbaa8a 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireStore.ts @@ -46,6 +46,7 @@ import { createSelectors } from './selector'; import { mutateRepeatEnableWhenExpressionInstances } from '../utils/enableWhenExpression'; import { questionnaireResponseStore } from './questionnaireResponseStore'; import { createQuestionnaireResponseItemMap } from '../utils/questionnaireResponseStoreUtils/updatableResponseItems'; +import { insertCompleteAnswerOptionsIntoQuestionnaire } from '../utils/questionnaireStoreUtils/insertAnswerOptions'; /** * QuestionnaireStore properties and methods @@ -180,6 +181,13 @@ export const questionnaireStore = createStore()((set, ge terminologyServerUrl ); + // Insert answerOptions with displays into questionnaire + questionnaire = insertCompleteAnswerOptionsIntoQuestionnaire( + questionnaire, + questionnaireModel.answerOptions + ); + + // Initialise form with questionnaire response and properties in questionnaire model const { initialEnableWhenItems, initialEnableWhenLinkedQuestions, diff --git a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/addDisplayToCodings.ts b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/addDisplayToCodings.ts index aabfb7bec..30aa27229 100644 --- a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/addDisplayToCodings.ts +++ b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/addDisplayToCodings.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import type { Coding } from 'fhir/r4'; +import type { Coding, QuestionnaireItemAnswerOption } from 'fhir/r4'; import type { CodeSystemLookupPromise } from '../../interfaces/lookup.interface'; import * as FHIR from 'fhirclient'; @@ -56,6 +56,45 @@ export async function addDisplayToProcessedCodings( return processedCodings; } +// Use this for a Record +export async function addDisplayToAnswerOptions( + answerOptions: Record, + terminologyServerUrl: string +): Promise> { + // Store code system lookup promises for codings without displays + const codeSystemLookupPromises: Record = {}; + for (const key in answerOptions) { + const options = answerOptions[key]; + for (const option of options) { + if (option.valueCoding && !option.valueCoding.display) { + const query = `system=${option.valueCoding.system}&code=${option.valueCoding.code}`; + codeSystemLookupPromises[query] = { + promise: getCodeSystemLookupPromise(query, terminologyServerUrl), + oldCoding: option.valueCoding + }; + } + } + } + + // Resolves lookup promises in one go and assign newCodings to processedCodings + const resolvedCodeSystemLookupPromises = await resolveLookupPromises(codeSystemLookupPromises); + for (const key in answerOptions) { + const options = answerOptions[key]; + + for (const option of options) { + if (option.valueCoding) { + const lookUpKey = `system=${option.valueCoding.system}&code=${option.valueCoding.code}`; + const resolvedLookup = resolvedCodeSystemLookupPromises[lookUpKey]; + if (resolvedLookup?.newCoding?.display) { + option.valueCoding.display = resolvedLookup.newCoding.display; + } + } + } + } + + return answerOptions; +} + // Use this for an array of codings export async function addDisplayToCodingArray( codings: Coding[], diff --git a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/createQuestionaireModel.ts b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/createQuestionaireModel.ts index 028fd094d..da5c951ed 100644 --- a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/createQuestionaireModel.ts +++ b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/createQuestionaireModel.ts @@ -28,7 +28,7 @@ import type { Variables } from '../../interfaces/variables.interface'; import { resolveValueSets } from './resolveValueSets'; import { addAdditionalVariables } from './addAdditionalVariables'; import { getLinkIdTypeTuples } from '../qItem'; -import { addDisplayToProcessedCodings } from './addDisplayToCodings'; +import { addDisplayToAnswerOptions, addDisplayToProcessedCodings } from './addDisplayToCodings'; export async function createQuestionnaireModel( questionnaire: Questionnaire, @@ -62,8 +62,13 @@ export async function createQuestionnaireModel( terminologyServerUrl ); - const { enableWhenItems, enableWhenExpressions, calculatedExpressions, answerExpressions } = - extractOtherExtensionsResult; + const { + enableWhenItems, + enableWhenExpressions, + calculatedExpressions, + answerExpressions, + answerOptions + } = extractOtherExtensionsResult; variables = extractOtherExtensionsResult.variables; valueSetPromises = extractOtherExtensionsResult.valueSetPromises; @@ -83,6 +88,12 @@ export async function createQuestionnaireModel( terminologyServerUrl ); + // In answerOptions, add display values to codings lacking them + const completeAnswerOptions = await addDisplayToAnswerOptions( + answerOptions, + terminologyServerUrl + ); + return { itemTypes, tabs, @@ -92,6 +103,7 @@ export async function createQuestionnaireModel( enableWhenExpressions, calculatedExpressions, answerExpressions, + answerOptions: completeAnswerOptions, processedValueSetCodings, processedValueSetUrls, fhirPathContext: {} @@ -107,6 +119,7 @@ function createEmptyModel(): QuestionnaireModel { calculatedExpressions: {}, enableWhenExpressions: { singleExpressions: {}, repeatExpressions: {} }, answerExpressions: {}, + answerOptions: {}, enableWhenItems: { singleItems: {}, repeatItems: {} }, processedValueSetCodings: {}, processedValueSetUrls: {}, diff --git a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractOtherExtensions.ts b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractOtherExtensions.ts index 01ff05381..771867758 100644 --- a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractOtherExtensions.ts +++ b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/extractOtherExtensions.ts @@ -19,6 +19,7 @@ import type { Expression, Questionnaire, QuestionnaireItem, + QuestionnaireItemAnswerOption, QuestionnaireItemEnableWhen } from 'fhir/r4'; import type { CalculatedExpression } from '../../interfaces/calculatedExpression.interface'; @@ -55,6 +56,7 @@ interface ReturnParamsRecursive { calculatedExpressions: Record; answerExpressions: Record; valueSetPromises: Record; + answerOptions: Record; } export function extractOtherExtensions( @@ -70,6 +72,7 @@ export function extractOtherExtensions( }; const calculatedExpressions: Record = {}; const answerExpressions: Record = {}; + const answerOptions: Record = {}; if (!questionnaire.item || questionnaire.item.length === 0) { return { @@ -81,6 +84,7 @@ export function extractOtherExtensions( }, calculatedExpressions: {}, answerExpressions: {}, + answerOptions: {}, valueSetPromises: valueSetPromises }; } @@ -95,6 +99,7 @@ export function extractOtherExtensions( enableWhenExpressions, calculatedExpressions, answerExpressions, + answerOptions, valueSetPromises, defaultTerminologyServerUrl: terminologyServerUrl, parentRepeatGroupLinkId: isRepeatGroup ? topLevelItem.linkId : undefined @@ -107,6 +112,7 @@ export function extractOtherExtensions( enableWhenExpressions, calculatedExpressions, answerExpressions, + answerOptions, valueSetPromises }; } @@ -119,6 +125,7 @@ interface extractExtensionsFromItemRecursiveParams { enableWhenExpressions: EnableWhenExpressions; calculatedExpressions: Record; answerExpressions: Record; + answerOptions: Record; valueSetPromises: Record; defaultTerminologyServerUrl: string; parentRepeatGroupLinkId?: string; @@ -135,6 +142,7 @@ function extractExtensionsFromItemRecursive( enableWhenExpressions, calculatedExpressions, answerExpressions, + answerOptions, valueSetPromises, defaultTerminologyServerUrl, parentRepeatGroupLinkId @@ -201,6 +209,12 @@ function extractExtensionsFromItemRecursive( }; } + // Get answerOptions + const options = item.answerOption ?? null; + if (options) { + answerOptions[item.linkId] = options; + } + const valueSetUrl = item.answerValueSet; if (valueSetUrl) { if (!valueSetPromises[valueSetUrl] && !valueSetUrl.startsWith('#')) { @@ -229,6 +243,7 @@ function extractExtensionsFromItemRecursive( enableWhenExpressions, calculatedExpressions, answerExpressions, + answerOptions, valueSetPromises }; } diff --git a/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/insertAnswerOptions.ts b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/insertAnswerOptions.ts new file mode 100644 index 000000000..3359a1564 --- /dev/null +++ b/packages/smart-forms-renderer/src/utils/questionnaireStoreUtils/insertAnswerOptions.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Questionnaire, QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; + +/** + * Filter x-fhir-query variables from questionnaire's extensions needed for population + * + * @author Sean Fong + */ +export function insertCompleteAnswerOptionsIntoQuestionnaire( + questionnaire: Questionnaire, + completeAnswerOptions: Record +): Questionnaire { + if (questionnaire.item && questionnaire.item.length > 0) { + for (const qItem of questionnaire.item) { + insertCompleteAnswerOptionsRecursive(qItem, completeAnswerOptions); + } + } + + return questionnaire; +} + +function insertCompleteAnswerOptionsRecursive( + qItem: QuestionnaireItem, + completeAnswerOptions: Record +) { + if (qItem.item) { + for (const childItem of qItem.item) { + insertCompleteAnswerOptionsRecursive(childItem, completeAnswerOptions); + } + } + + if (qItem.answerOption) { + const options = completeAnswerOptions[qItem.linkId]; + if (options) { + qItem.answerOption = options; + } + } + + return; +}