Skip to content
Merged
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
168 changes: 156 additions & 12 deletions src/__tests__/useQuestionItemContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Questionnaire, QuestionnaireResponse, QuestionnaireResponseItem }
import { isSuccess, success } from '@beda.software/remote-data';
import { describe, expect, test, vi } from 'vitest';

import { useQuestionItemContext } from '../hooks';
import { useVariablesResolver } from '../hooks';
import type { ItemContext } from '../types';
import type { FCEQuestionnaireItem } from '../fce.types';
import { getBranchItems } from '../utils.js';
import { getBranchItems, getEnabledQuestions } from '../utils.js';

function createInitialContext(questionnaire: Questionnaire, questionnaireResponse: QuestionnaireResponse): ItemContext {
return {
Expand All @@ -29,7 +29,7 @@ function buildFHIRServiceMock(mapping: Record<string, any>) {
});
}

describe('useQuestionItemContext', () => {
describe('useVariablesResolver', () => {
test('returns single context for non-group question', () => {
const questionnaire: Questionnaire = {
resourceType: 'Questionnaire',
Expand Down Expand Up @@ -63,10 +63,11 @@ describe('useQuestionItemContext', () => {
};

const { result } = renderHook(() =>
useQuestionItemContext({
useVariablesResolver({
initialContext,
branchItems,
questionItem,
variable: questionItem.variable,
prefix: questionItem.linkId,
fhirService: vi.fn(),
}),
);
Expand Down Expand Up @@ -152,10 +153,11 @@ describe('useQuestionItemContext', () => {
};

const { result } = renderHook(() =>
useQuestionItemContext({
useVariablesResolver({
initialContext,
branchItems,
questionItem,
variable: questionItem.variable,
prefix: questionItem.linkId,
fhirService: vi.fn(),
}),
);
Expand All @@ -175,7 +177,7 @@ describe('useQuestionItemContext', () => {
});
});

describe('useQuestionItemContext with x-fhir-query', () => {
describe('useVariablesResolver with x-fhir-query', () => {
test('evaluates patient/org variable chain', async () => {
const questionnaire: Questionnaire = {
resourceType: 'Questionnaire',
Expand Down Expand Up @@ -269,13 +271,18 @@ describe('useQuestionItemContext with x-fhir-query', () => {
const initialBranchItems = getBranchItems([orgItem.linkId], questionnaire, initialQuestionnaireResponse);

const { result, rerender } = renderHook(
(props: {
({
questionItem,
...props
}: {
initialContext: ItemContext;
branchItems: ReturnType<typeof getBranchItems>;
questionItem: FCEQuestionnaireItem;
}) =>
useQuestionItemContext({
useVariablesResolver({
...props,
variable: questionItem.variable,
prefix: questionItem.linkId,
fhirService,
}),
{
Expand Down Expand Up @@ -452,10 +459,11 @@ describe('useQuestionItemContext with x-fhir-query', () => {
const branchItems = getBranchItems(['row'], questionnaire, questionnaireResponse);

const { result } = renderHook(() =>
useQuestionItemContext({
useVariablesResolver({
initialContext,
branchItems,
questionItem,
variable: questionItem.variable,
prefix: questionItem.linkId,
fhirService,
}),
);
Expand Down Expand Up @@ -489,3 +497,139 @@ describe('useQuestionItemContext with x-fhir-query', () => {
);
});
});

describe('useVariablesResolver with itemPopulationContext', () => {
const patient = {
resourceType: 'Patient',
address: [
{ use: 'home', line: ['1 Home St'] },
{ type: 'postal', line: ['PO Box 5'] },
],
};

function buildPostalContext(patientResource: any): ItemContext {
const questionnaire: Questionnaire = { resourceType: 'Questionnaire', status: 'active', item: [] };
const questionnaireResponse: QuestionnaireResponse = {
resourceType: 'QuestionnaireResponse',
status: 'in-progress',
item: [{ linkId: 'postal', item: [{ linkId: 'city' }] }],
};

return {
...createInitialContext(questionnaire, questionnaireResponse),
patient: patientResource,
} as ItemContext;
}

const postalQuestionnaire: Questionnaire = {
resourceType: 'Questionnaire',
status: 'active',
item: [
{
linkId: 'postal',
type: 'group',
itemPopulationContext: {
name: 'PostalAddressArray',
language: 'text/fhirpath',
expression: "%patient.address.where(type='postal')",
},
item: [{ linkId: 'city', type: 'string' }],
} as any,
],
};

const postalQuestionnaireResponse: QuestionnaireResponse = {
resourceType: 'QuestionnaireResponse',
status: 'in-progress',
item: [{ linkId: 'postal', item: [{ linkId: 'city' }] }],
};

test('binds the itemPopulationContext variable into the resolved context', () => {
const initialContext = buildPostalContext(patient);
const branchItems = getBranchItems(['postal'], postalQuestionnaire, postalQuestionnaireResponse);
const questionItem = postalQuestionnaire.item![0] as FCEQuestionnaireItem;

const { result } = renderHook(() =>
useVariablesResolver({
initialContext,
branchItems,
variable: questionItem.variable,
prefix: questionItem.linkId,
fhirService: vi.fn(),
}),
);

expect(result.current.contexts).toHaveLength(1);
expect(result.current.contexts[0].PostalAddressArray).toEqual([{ type: 'postal', line: ['PO Box 5'] }]);
});

test('a variable can reference the bound itemPopulationContext', () => {
const questionnaire: Questionnaire = {
resourceType: 'Questionnaire',
status: 'active',
item: [
{
linkId: 'postal',
type: 'group',
itemPopulationContext: {
name: 'PostalAddressArray',
language: 'text/fhirpath',
expression: "%patient.address.where(type='postal')",
},
variable: [
{
name: 'PostalLine',
language: 'text/fhirpath',
expression: '%PostalAddressArray.line',
},
],
item: [{ linkId: 'city', type: 'string' }],
} as any,
],
};

const initialContext = buildPostalContext(patient);
const branchItems = getBranchItems(['postal'], questionnaire, postalQuestionnaireResponse);
const questionItem = questionnaire.item![0] as FCEQuestionnaireItem;

const { result } = renderHook(() =>
useVariablesResolver({
initialContext,
branchItems,
variable: questionItem.variable,
prefix: questionItem.linkId,
fhirService: vi.fn(),
}),
);

expect((result.current.contexts[0].PostalLine as string[])[0]).toEqual('PO Box 5');
});

test('enableWhenExpression can reference an item itemPopulationContext variable', () => {
const item = {
linkId: 'postal',
type: 'group',
itemPopulationContext: {
name: 'PostalAddressArray',
language: 'text/fhirpath',
expression: "%patient.address.where(type='postal')",
},
enableWhenExpression: {
language: 'text/fhirpath',
expression: '%PostalAddressArray.exists()',
},
item: [{ linkId: 'city', type: 'string' }],
} as unknown as FCEQuestionnaireItem;

const enabled = getEnabledQuestions([item], [], {}, buildPostalContext(patient));
expect(enabled).toHaveLength(1);

const disabled = getEnabledQuestions(
[item],
[],
{},
buildPostalContext({ resourceType: 'Patient', address: [{ use: 'home', line: ['1 Home St'] }] }),
);
expect(disabled).toHaveLength(0);
});
});
8 changes: 5 additions & 3 deletions src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
stripNonEnumerable,
wrapAnswerValue,
} from './utils.js';
import { useQuestionItemContext } from './hooks';
import { useVariablesResolver } from './hooks';

function usePreviousValue<T>(value: T) {
const prevValue = useRef<T | undefined>(value);
Expand Down Expand Up @@ -65,11 +65,13 @@ export function QuestionItem(props: QuestionItemProps) {
() => getBranchItems(fieldPath, initialContext.questionnaire, initialContext.resource),
[fieldPath, initialContext.questionnaire, initialContext.resource],
);
const { contexts } = useQuestionItemContext({
const { contexts } = useVariablesResolver({
initialContext,
branchItems,
fhirService,
questionItem,
evaluateFhirpath,
variable: questionItem.variable,
prefix: linkId,
});
const context = type === 'group' ? contexts : contexts[0]!;
const prevAnswers: FormAnswerItems[] | undefined = usePreviousValue(_.get(formValues, fieldPath));
Expand Down
32 changes: 19 additions & 13 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import type { AxiosRequestConfig } from 'axios';
import { FCEQuestionnaireItem } from './fce.types';
import { type RemoteData, isSuccess, loading, success, mapSuccess, sequenceArray } from '@beda.software/remote-data';

import { QRFContext } from './context';
import { EvaluateFhirpath, ItemContext } from './types';
import { resolveTemplateExpr, evaluateFHIRPathExpression, getBranchItems } from './utils';
import { resolveItemPopulationContext, resolveTemplateExpr, evaluateFHIRPathExpression } from './utils';
import { Expression, QuestionnaireItem, QuestionnaireResponse, QuestionnaireResponseItem } from 'fhir/r4b';

export function useQuestionnaireResponseFormContext() {
return useContext(QRFContext);
}

export type UseQuestionItemContextArgs = {
export type UseVariablesResolverArgs = {
initialContext: ItemContext;
branchItems: ReturnType<typeof getBranchItems>;
branchItems: { qItem?: QuestionnaireItem; qrItems: Array<QuestionnaireResponseItem | QuestionnaireResponse> };
fhirService: (config: AxiosRequestConfig) => Promise<RemoteData<unknown>>;
questionItem: FCEQuestionnaireItem;
evaluateFhirpath?: EvaluateFhirpath;
variable?: Expression[];
prefix?: string;
};

type AsyncState = Record<
Expand All @@ -30,22 +31,24 @@ type AsyncState = Record<
>
>;

export function useQuestionItemContext(props: UseQuestionItemContextArgs): {
export function useVariablesResolver(props: UseVariablesResolverArgs): {
contexts: ItemContext[];
evaluationResponse: RemoteData<ItemContext[]>;
} {
const { initialContext, branchItems, fhirService, questionItem, evaluateFhirpath } = props;
const { variable, linkId } = questionItem;
const { initialContext, branchItems, fhirService, evaluateFhirpath, variable, prefix } = props;
const variables = useMemo(() => variable ?? [], [variable]);
const [asyncState, setAsyncState] = useState<AsyncState>({});

useEffect(() => {
branchItems.qrItems.forEach((qrItem, branchIndex) => {
const workingContext: ItemContext = {
let workingContext: ItemContext = {
...initialContext,
context: qrItem,
qitem: branchItems.qItem,
};
workingContext = branchItems.qItem
? resolveItemPopulationContext(workingContext, branchItems.qItem, evaluateFhirpath)
: workingContext;

variables.forEach((variable) => {
if (!variable?.name || !variable.expression) {
Expand All @@ -55,7 +58,7 @@ export function useQuestionItemContext(props: UseQuestionItemContextArgs): {
const { name, expression, language } = variable;

if (language === 'application/x-fhir-query') {
const url = resolveTemplateExpr(expression!, workingContext, `${linkId}.variable.${name}`, true);
const url = resolveTemplateExpr(expression!, workingContext, `${prefix}.variable.${name}`, true);

if (!url) {
workingContext[name] = null;
Expand Down Expand Up @@ -115,7 +118,7 @@ export function useQuestionItemContext(props: UseQuestionItemContextArgs): {
workingContext[name] = evaluateFHIRPathExpression(
variable,
workingContext,
`${linkId}.variable.${name}`,
`${prefix}.variable.${name}`,
evaluateFhirpath,
);
}
Expand All @@ -125,11 +128,14 @@ export function useQuestionItemContext(props: UseQuestionItemContextArgs): {

const contexts = useMemo(() => {
return branchItems.qrItems.map<ItemContext>((qrItem, branchIndex) => {
const workingContext: ItemContext = {
let workingContext: ItemContext = {
...initialContext,
context: qrItem,
qitem: branchItems.qItem,
};
workingContext = branchItems.qItem
? resolveItemPopulationContext(workingContext, branchItems.qItem, evaluateFhirpath)
: workingContext;

variables.forEach((variable) => {
if (!variable?.name || !variable.expression) {
Expand All @@ -151,7 +157,7 @@ export function useQuestionItemContext(props: UseQuestionItemContextArgs): {
workingContext[name] = evaluateFHIRPathExpression(
variable,
workingContext,
`${linkId}.variable.${name}`,
`${prefix}.variable.${name}`,
evaluateFhirpath,
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export {
removeItemKey,
getItemKey,
} from './utils';
export { useQuestionnaireResponseFormContext } from './hooks';
export { useQuestionnaireResponseFormContext, useVariablesResolver } from './hooks';
export type { UseVariablesResolverArgs } from './hooks';
export { QuestionItems, QuestionItem, QuestionnaireResponseFormProvider } from './components';
export * from './converter';
export type * from './fce.types';
Loading
Loading