diff --git a/helpers/fhirliquid-engine.ts b/helpers/fhirliquid-engine.ts index c63e849..b23af50 100644 --- a/helpers/fhirliquid-engine.ts +++ b/helpers/fhirliquid-engine.ts @@ -571,7 +571,9 @@ export class FhirLiquidEngine { expression: ExpressionContext, state: EvaluationState, ): boolean { - const values = this.evaluateExpression(expression, state); + const values = this.evaluateExpression(expression, state) + .map(value => fhirpath.resolveInternalTypes(value)) + .filter(value => value !== null && value !== undefined); if (values.length === 0) return false; if (values.length !== 1 || typeof values[0] !== "boolean") { throw new Error( @@ -599,7 +601,7 @@ export class FhirLiquidEngine { expressionText, variables, this.model, - { async: false }, + { async: false, resolveInternalTypes: false }, ); } @@ -616,13 +618,14 @@ export class FhirLiquidEngine { ): string { const context: FhirLiquidRenderContext = { resource: state.resource, - variables: variablesFromState(state), + variables: resolvedVariablesFromState(state), }; const rendered: string[] = []; for (const value of values) { - if (value === null || value === undefined) continue; - const custom = this.renderValue?.(value, context); - rendered.push(custom ?? defaultValueToString(value)); + const resolvedValue = fhirpath.resolveInternalTypes(value); + if (resolvedValue === null || resolvedValue === undefined) continue; + const custom = this.renderValue?.(resolvedValue, context); + rendered.push(custom ?? defaultValueToString(resolvedValue)); } return rendered.join(", "); } @@ -644,6 +647,15 @@ function variablesFromState(state: EvaluationState): Record { }; } +function resolvedVariablesFromState( + state: EvaluationState, +): Record { + return Object.fromEntries( + Object.entries(variablesFromState(state)) + .map(([name, value]) => [name, fhirpath.resolveInternalTypes(value)]), + ); +} + function itemsBetween( items: TItem[], startExclusive: number, diff --git a/test/fhirliquid-engine.test.ts b/test/fhirliquid-engine.test.ts index 9b70550..46adff3 100644 --- a/test/fhirliquid-engine.test.ts +++ b/test/fhirliquid-engine.test.ts @@ -16,6 +16,22 @@ const patient = { ], }; +const patientWithExtensions = { + ...patient, + meta: { + extension: [ + { + url: "u1", + valueString: "sample extension string", + }, + { + url: "u12", + valueMarkdown: "### a simple header", + }, + ], + }, +}; + describe("FhirLiquidEngine", () => { it("renders FHIRPath output and Liquid filters", () => { const engine = new FhirLiquidEngine(); @@ -59,6 +75,18 @@ describe("FhirLiquidEngine", () => { expect(evaluateFhirLiquid(template, patient)).toBe("1=Chalmers,3=Jones;"); }); + it("retains FHIR type information for loop variables", () => { + const template = "{% for i in Patient.meta.extension %}" + + "[{{ i.value }}|{{ %i.value }}|" + + "{{ i.valueString }}|{{ i.valueMarkdown }}]" + + "{% endfor %}"; + + expect(evaluateFhirLiquid(template, patientWithExtensions)).toBe( + "[sample extension string|sample extension string|sample extension string|]" + + "[### a simple header|### a simple header||### a simple header]", + ); + }); + it("applies loop modifiers and renders the empty branch", () => { expect(evaluateFhirLiquid( "{% for name in Patient.name reversed offset: 1 limit: 2 %}" @@ -92,6 +120,24 @@ describe("FhirLiquidEngine", () => { )).toContain("Important"); }); + it("passes resolved values to custom renderers", () => { + const renderValue = jest.fn(() => undefined); + const engine = new FhirLiquidEngine({ renderValue }); + + expect(engine.evaluate( + "{% for name in Patient.name limit: 1 %}{{ name }}{% endfor %}", + patient, + )) + .toContain("\"family\":\"Chalmers\""); + expect(renderValue).toHaveBeenCalledWith( + patient.name[0], + expect.objectContaining({ + resource: patient, + variables: expect.objectContaining({ name: patient.name[0] }), + }), + ); + }); + it("rejects invalid templates and multi-value assignments", () => { const engine = new FhirLiquidEngine(); diff --git a/vue3-src/app/components/TwinPaneTab.vue b/vue3-src/app/components/TwinPaneTab.vue index a2faecd..2640571 100644 --- a/vue3-src/app/components/TwinPaneTab.vue +++ b/vue3-src/app/components/TwinPaneTab.vue @@ -343,8 +343,8 @@ const getActiveTabs = (): TabData[] => { } else { const lockedTabData = props.tabs[lockedTab.value] const selectableTabData = props.tabs[selectableTab.value] - if (lockedTabData) result.push(lockedTabData) - if (selectableTabData) result.push(selectableTabData) + if (lockedTabData && lockedTabData.show) result.push(lockedTabData) + if (selectableTabData && selectableTabData.show) result.push(selectableTabData) } return result } diff --git a/vue3-src/app/pages/fhir-liquid.vue b/vue3-src/app/pages/fhir-liquid.vue index 6301e95..ee50fb3 100644 --- a/vue3-src/app/pages/fhir-liquid.vue +++ b/vue3-src/app/pages/fhir-liquid.vue @@ -102,6 +102,7 @@ useHead({ interface TwinPaneControl { selectTab(tabIndex: number): void; + getActiveTabs(): TabData[]; } interface ResourceEditorControl { @@ -119,6 +120,7 @@ const tabSpaces = 2; const TEMPLATE_TAB = 0; const RESOURCE_TAB = 1; const OUTPUT_TAB = 2; +const OUTPUT_HTML_TAB = 3; const ERRORS_TAB = 4; const twinTabControl = ref(); const templateEditor = ref(); @@ -235,8 +237,12 @@ function evaluateTemplate(): void { } errorOutcome.value = undefined; - twinTabControl.value?.selectTab(OUTPUT_TAB); - showSuccessMessage("Template evaluated successfully."); + var activeTabs = twinTabControl.value?.getActiveTabs() ?? []; + // only switch tabs if either of the output tabs are not currently active + if (!activeTabs.some(tab => tab.tabName === "Output" || tab.tabName === "Output HTML")) { + twinTabControl.value?.selectTab(hasHtmlOutput.value ? OUTPUT_HTML_TAB : OUTPUT_TAB); + } + // twinTabControl.value?.selectTab(OUTPUT_TAB); } function ctrlEnterHandler(event: KeyboardEvent): void {