diff --git a/containers/ecr-viewer/src/app/components/AccordionContainer.tsx b/containers/ecr-viewer/src/app/components/AccordionContainer.tsx index d6bd33ed8..fb2735128 100644 --- a/containers/ecr-viewer/src/app/components/AccordionContainer.tsx +++ b/containers/ecr-viewer/src/app/components/AccordionContainer.tsx @@ -1,9 +1,10 @@ import React from "react"; import { Accordion, HeadingLevel } from "@trussworks/react-uswds"; import { formatString } from "@/app/services/formatService"; +import { RenderableNode } from "../view-data/utils/utils"; export interface AccordionItemProps { - title: React.ReactNode | string; + title: RenderableNode; content: React.ReactNode; expanded: boolean; id: string; diff --git a/containers/ecr-viewer/src/app/services/formatService.tsx b/containers/ecr-viewer/src/app/services/formatService.tsx index c68ab2f0c..c0fdb990e 100644 --- a/containers/ecr-viewer/src/app/services/formatService.tsx +++ b/containers/ecr-viewer/src/app/services/formatService.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ToolTipElement } from "@/app/view-data/components/ToolTipElement"; import { ContactPoint } from "fhir/r4"; -import { sanitizeAndMap } from "../view-data/utils/utils"; +import { RenderableNode, safeParse } from "../view-data/utils/utils"; interface Metadata { [key: string]: string; @@ -359,9 +359,7 @@ export function formatTablesToJSON(htmlString: string): TableJson[] { const tables: any[] = []; const resultId = getDataId(li); const firstChildNode = getFirstNonCommentChild(li); - const resultName = firstChildNode - ? getElementContent(firstChildNode) - : ""; + const resultName = firstChildNode ? getElementText(firstChildNode) : ""; li.querySelectorAll("table").forEach((table) => { tables.push(processTable(table)); }); @@ -377,7 +375,7 @@ export function formatTablesToJSON(htmlString: string): TableJson[] { ) as NodeListOf; if (tableWithCaptionArray.length > 0) { tableWithCaptionArray.forEach((table) => { - const resultName = getElementContent(table.caption as Node); + const resultName = getElementText(table.caption as Element); const resultId = getDataId(table) ?? undefined; jsonArray.push({ resultId, resultName, tables: [processTable(table)] }); }); @@ -389,7 +387,7 @@ export function formatTablesToJSON(htmlString: string): TableJson[] { const contentArray = doc.querySelectorAll("content"); if (contentArray.length > 0) { contentArray.forEach((content) => { - const resultName = getElementContent(content); + const resultName = getElementText(content); const tables: any[] = []; let sibling = content.nextElementSibling; @@ -474,7 +472,7 @@ function processTable(table: Element): TableRow[] { if (headers.length > 0) { hasHeaders = true; headers.forEach((header) => { - keys.push(getElementContent(header)); + keys.push(getElementText(header)); }); } @@ -506,8 +504,30 @@ function processTable(table: Element): TableRow[] { return jsonArray; } -function getElementContent(el: Node): string { - return sanitizeAndMap(el.textContent?.trim() ?? ""); +/** + * Extracts the html content from an element and sanitizes and maps it so it is safe to render. + * @param el - An HTML element or node. + * @returns A sanitized and parsed snippet of JSX. + * @example @param el - Values here + * @example @returns -

Values here

+ */ +function getElementContent(el: Element | Node): RenderableNode { + const rawValue = (el as Element)?.innerHTML ?? el.textContent; + const value = rawValue?.trim() ?? ""; + if (value === "") return value; + let res = safeParse(value); + return res; +} + +/** + * Extracts the text content from an element and concatenates it. + * @param el - An HTML element or node. + * @returns A string with the text data. + * @example @param el - Values here + * @example @returns - 'Values here' + */ +function getElementText(el: Element | Node): string { + return el.textContent?.trim() ?? ""; } /** diff --git a/containers/ecr-viewer/src/app/services/labsService.tsx b/containers/ecr-viewer/src/app/services/labsService.tsx index 0de659b8a..b9e07dd28 100644 --- a/containers/ecr-viewer/src/app/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/services/labsService.tsx @@ -2,11 +2,12 @@ import React from "react"; import { Bundle, Device, Observation, Organization, Reference } from "fhir/r4"; import { PathMappings, + RenderableNode, + arrayToElement, noData, - sanitizeAndMap, + safeParse, } from "@/app/view-data/utils/utils"; import { evaluate } from "@/app/view-data/utils/evaluate"; -import parse from "html-react-parser"; import { AccordionLabResults } from "@/app/view-data/components/AccordionLabResults"; import { formatDateTime, @@ -149,33 +150,31 @@ export const checkAbnormalTag = (labReportJson: TableJson): boolean => { * @example result - JSON object that contains the tables for all lab reports * @example searchKey - Ex. "Analysis Time" or the field that we are searching data for. */ -export function searchResultRecord(result: any[], searchKey: string) { - let resultsArray: any[] = []; +export function searchResultRecord( + result: any[], + searchKey: string, +): RenderableNode { + let resultsArray: RenderableNode[] = []; // Loop through each table for (const table of result) { // For each table, recursively search through all nodes if (Array.isArray(table)) { - const nestedResult: string = searchResultRecord(table, searchKey); + const nestedResult = searchResultRecord(table, searchKey); if (nestedResult) { return nestedResult; } - } else { - const keys = Object.keys(table); - let searchKeyValue: string = ""; - keys.forEach((key) => { - // Search for search key value - if (key === searchKey && table[key].hasOwnProperty("value")) { - searchKeyValue = table[key]["value"]; - } - }); - - if (searchKeyValue !== "") { - resultsArray.push(searchKeyValue); - } + } else if ( + table.hasOwnProperty(searchKey) && + table[searchKey].hasOwnProperty("value") + ) { + resultsArray.push(table[searchKey]["value"]); } } - return [...new Set(resultsArray)].join(", "); + + // Remove empties and duplicates + const res = [...new Set(resultsArray.filter(Boolean))]; + return arrayToElement(res); } /** @@ -189,7 +188,7 @@ const returnSpecimenSource = ( report: LabReport, fhirBundle: Bundle, mappings: PathMappings, -): React.ReactNode => { +): RenderableNode => { const observations = getObservations(report, fhirBundle, mappings); const specimenSource = observations.flatMap((observation) => { return evaluate(observation, mappings["specimenSource"]); @@ -211,7 +210,7 @@ const returnCollectionTime = ( report: LabReport, fhirBundle: Bundle, mappings: PathMappings, -): React.ReactNode => { +): RenderableNode => { const observations = getObservations(report, fhirBundle, mappings); const collectionTime = observations.flatMap((observation) => { const rawTime = evaluate(observation, mappings["specimenCollectionTime"]); @@ -236,7 +235,7 @@ const returnReceivedTime = ( report: LabReport, fhirBundle: Bundle, mappings: PathMappings, -): React.ReactNode => { +): RenderableNode => { const observations = getObservations(report, fhirBundle, mappings); const receivedTime = observations.flatMap((observation) => { const rawTime = evaluate(observation, mappings["specimenReceivedTime"]); @@ -259,14 +258,14 @@ const returnReceivedTime = ( export const returnFieldValueFromLabHtmlString = ( labReportJson: TableJson, fieldName: string, -): React.ReactNode => { +): RenderableNode => { if (!labReportJson) { return noData; } const labTables = labReportJson.tables; const fieldValue = searchResultRecord(labTables ?? [], fieldName); - if (!fieldValue || fieldValue.length === 0) { + if (!fieldValue) { return noData; } @@ -282,20 +281,33 @@ export const returnFieldValueFromLabHtmlString = ( const returnAnalysisTime = ( labReportJson: TableJson, fieldName: string, -): React.ReactNode => { - const fieldVals = returnFieldValueFromLabHtmlString(labReportJson, fieldName); +): RenderableNode => { + const fieldVal = returnFieldValueFromLabHtmlString(labReportJson, fieldName); - if (fieldVals === noData) { + if (fieldVal === noData) { return noData; } - const analysisTimeArray = - typeof fieldVals === "string" ? fieldVals.split(", ") : []; - const analysisTimeArrayFormatted = analysisTimeArray.map((dateTime) => { + if (typeof fieldVal === "string") return formatDateTime(fieldVal); + + let analysisTimeArrayFormatted = ( + fieldVal as React.JSX.Element + ).props.children.map((el: RenderableNode) => { + let dateTime; + if (typeof el === "string") { + dateTime = el; + } else if ( + el?.props?.children?.length === 1 && + typeof el?.props?.children[0] === "string" + ) { + dateTime = el.props.children[0]; + } else { + return ""; + } return formatDateTime(dateTime); }); - return [...new Set(analysisTimeArrayFormatted)].join(", "); + return [...new Set(analysisTimeArrayFormatted.filter(Boolean))].join(", "); }; /** @@ -371,7 +383,7 @@ export const evaluateDiagnosticReportData = ( infoPath: "observationDeviceReference", applyToValue: (ref) => { const device = evaluateReference(fhirBundle, mappings, ref) as Device; - return parse(sanitizeAndMap(device.deviceName?.[0]?.name ?? "")); + return safeParse(device.deviceName?.[0]?.name ?? ""); }, className: "minw-10 width-20", }, @@ -379,7 +391,7 @@ export const evaluateDiagnosticReportData = ( columnName: "Lab Comment", infoPath: "observationNote", hiddenBaseText: "comment", - applyToValue: (v) => parse(sanitizeAndMap(v)), + applyToValue: (v) => safeParse(v), className: "minw-10 width-20", }, ]; diff --git a/containers/ecr-viewer/src/app/tests/__snapshots__/utils.test.tsx.snap b/containers/ecr-viewer/src/app/tests/__snapshots__/utils.test.tsx.snap new file mode 100644 index 000000000..9bea07aaf --- /dev/null +++ b/containers/ecr-viewer/src/app/tests/__snapshots__/utils.test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Utils safeParse should map xml-y HTML 1`] = ` + +

+ hi there +

+ + I'm content + + +
+`; + +exports[`Utils safeParse should remove comments 1`] = ` + +

+ hi there +

+ I'm content + +
+`; + +exports[`Utils safeParse should remove empty nodes 1`] = ` + +
+ + hiya + +
+`; diff --git a/containers/ecr-viewer/src/app/tests/assets/BundleLab.json b/containers/ecr-viewer/src/app/tests/assets/BundleLab.json index 84f73037d..e77a11106 100644 --- a/containers/ecr-viewer/src/app/tests/assets/BundleLab.json +++ b/containers/ecr-viewer/src/app/tests/assets/BundleLab.json @@ -47,7 +47,7 @@ "title": "Results", "text": { "status": "generated", - "div": " documented in this encounter" + "div": " documented in this encounter" }, "code": { "coding": [ diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/AccordionContent.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/AccordionContent.test.tsx.snap index d36840894..91aec2d74 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/AccordionContent.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/AccordionContent.test.tsx.snap @@ -53,7 +53,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Vital Status
Deceased
@@ -217,7 +217,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Patient Name
No data
@@ -236,7 +236,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp DOB
No data
@@ -255,7 +255,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Current Age
No data
@@ -274,7 +274,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Age at Death
No data
@@ -293,7 +293,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Date of Death
No data
@@ -312,7 +312,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Sex
No data
@@ -331,7 +331,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Race
No data
@@ -350,7 +350,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Ethnicity
No data
@@ -369,7 +369,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Tribal Affiliation
No data
@@ -388,7 +388,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Preferred Language
No data
@@ -407,7 +407,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Patient Address
No data
@@ -426,7 +426,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp County
No data
@@ -445,7 +445,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Contact
No data
@@ -464,7 +464,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Emergency Contact
No data
@@ -504,7 +504,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp
No data
@@ -536,7 +536,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Tobacco Use
No data
@@ -555,7 +555,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Travel History
No data
@@ -574,7 +574,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Homeless Status
No data
@@ -593,7 +593,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Pregnancy Status
No data
@@ -612,7 +612,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Alcohol Use
No data
@@ -631,7 +631,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Sexual Orientation
No data
@@ -650,7 +650,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Gender Identity
No data
@@ -669,7 +669,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Occupation
No data
@@ -701,7 +701,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Encounter Date/Time
No data
@@ -720,7 +720,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Encounter Type
No data
@@ -739,7 +739,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Encounter ID
No data
@@ -758,7 +758,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Encounter Diagnosis
No data
@@ -790,7 +790,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Facility Name
No data
@@ -809,7 +809,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Facility Address
No data
@@ -828,7 +828,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Facility Contact Address
No data
@@ -847,7 +847,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Facility Contact
No data
@@ -866,7 +866,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Facility Type
No data
@@ -885,7 +885,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Facility ID
No data
@@ -917,7 +917,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Miscellaneous Notes
No data
@@ -949,7 +949,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Provider Name
No data
@@ -968,7 +968,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Provider Address
No data
@@ -987,7 +987,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Provider Contact
No data
@@ -1006,7 +1006,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Provider Facility Name
No data
@@ -1025,7 +1025,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Provider Facility Address
No data
@@ -1044,7 +1044,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Provider ID
No data
@@ -1076,7 +1076,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Reason for Visit
No data
@@ -1095,7 +1095,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Problems List
No data
@@ -1127,7 +1127,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Vital Signs
No data
@@ -1159,7 +1159,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Immunization History
No data
@@ -1191,7 +1191,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Procedures
No data
@@ -1210,7 +1210,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Planned Procedures
No data
@@ -1229,7 +1229,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Plan of Treatment
No data
@@ -1248,7 +1248,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Administered Medications
No data
@@ -1267,7 +1267,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Care Team
No data
@@ -1320,7 +1320,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp
No data
@@ -1339,7 +1339,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Date/Time eCR Created
No data
@@ -1358,7 +1358,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp eICR Release Version
No data
@@ -1377,7 +1377,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp EHR Manufacturer Model Name
No data
@@ -1417,7 +1417,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp
No data
@@ -1436,7 +1436,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp eRSD Warnings
No data
@@ -1455,7 +1455,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Custodian ID
No data
@@ -1474,7 +1474,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Custodian Name
No data
@@ -1493,7 +1493,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Custodian Address
No data
@@ -1512,7 +1512,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Custodian Contact
No data
@@ -1544,7 +1544,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Author Name
No data
@@ -1563,7 +1563,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Author Address
No data
@@ -1582,7 +1582,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Author Contact
No data
@@ -1601,7 +1601,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Author Facility Name
No data
@@ -1620,7 +1620,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Author Facility Address
No data
@@ -1639,7 +1639,7 @@ exports[`Snapshot test for Accordion Content Given no data, info message for emp Author Facility Contact
No data
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Clinical.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Clinical.test.tsx.snap index 23967b45d..be12346de 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Clinical.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Clinical.test.tsx.snap @@ -30,7 +30,7 @@ exports[`Snapshot test for Clinical Notes should match snapshot for non table no Miscellaneous Notes

This patient was only recently discharged for a recurrent GI bleed as described diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Demographics.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Demographics.test.tsx.snap index c9d907662..12cab5a28 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Demographics.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Demographics.test.tsx.snap @@ -30,7 +30,7 @@ exports[`Demographics should match snapshot 1`] = ` Patient Name

Test patient
@@ -49,7 +49,7 @@ exports[`Demographics should match snapshot 1`] = ` DOB
06/01/1996
@@ -68,7 +68,7 @@ exports[`Demographics should match snapshot 1`] = ` Current Age
27
@@ -87,7 +87,7 @@ exports[`Demographics should match snapshot 1`] = ` Vital Status
Alive
@@ -106,7 +106,7 @@ exports[`Demographics should match snapshot 1`] = ` Sex
female
@@ -125,7 +125,7 @@ exports[`Demographics should match snapshot 1`] = ` Race
Asian/Pacific Islander
@@ -144,7 +144,7 @@ exports[`Demographics should match snapshot 1`] = ` Ethnicity
Not Hispanic or Latino
@@ -163,7 +163,7 @@ exports[`Demographics should match snapshot 1`] = ` Tribal
test
@@ -182,7 +182,7 @@ exports[`Demographics should match snapshot 1`] = ` Preferred Language
test
@@ -201,7 +201,7 @@ exports[`Demographics should match snapshot 1`] = ` Patient Address
test address
@@ -220,7 +220,7 @@ exports[`Demographics should match snapshot 1`] = ` County
test
@@ -239,7 +239,7 @@ exports[`Demographics should match snapshot 1`] = ` Contact
test contact
@@ -258,7 +258,7 @@ exports[`Demographics should match snapshot 1`] = ` Emergency Contact
N/A
@@ -277,7 +277,7 @@ exports[`Demographics should match snapshot 1`] = ` Patient IDs
123-4567-890
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrMetadata.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrMetadata.test.tsx.snap index 3695f9ff8..1adae7ac6 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrMetadata.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrMetadata.test.tsx.snap @@ -231,7 +231,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` eICR Identifier
1dd10047-2207-4eac-a993-0f706c88be5d
@@ -250,7 +250,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` Date/Time eCR Created
2022-05-14T12:56:38Z
@@ -269,7 +269,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` eICR Release Version
R1.1 (2016-12-01)
@@ -288,7 +288,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` EHR Software Name
Epic - Version 10.1
@@ -307,7 +307,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` EHR Manufacturer Model Name
Epic - Version 10.1
@@ -335,7 +335,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` Author Name
Dr. Stella Zinman
@@ -354,7 +354,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` Author Address
1 Main st
@@ -373,7 +373,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` Author Contact
(661)382-5000
@@ -392,7 +392,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` Author Facility Name
PRM- Palmdale Regional Medical Center
@@ -411,7 +411,7 @@ exports[`ECR Metadata should match snapshot 1`] = ` Author Facility Address
38600 Medical Center Drive Palmdale, CA @@ -432,7 +432,7 @@ Palmdale, CA Author Facility Contact
(661)382-5000
@@ -460,7 +460,7 @@ Palmdale, CA Custodian ID
1104202761
@@ -479,7 +479,7 @@ Palmdale, CA Custodian Name
Vanderbilt University Medical Center
@@ -498,7 +498,7 @@ Palmdale, CA Custodian Address
3401 West End Ave Nashville, TN @@ -519,7 +519,7 @@ Nashville, TN Custodian Contact
Work 1-615-322-5000
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrSummary.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrSummary.test.tsx.snap index 97d66a9c9..4085cdafc 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrSummary.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/EcrSummary.test.tsx.snap @@ -28,7 +28,7 @@ exports[`EcrSummary should match snapshot 1`] = ` Patient Name
ABEL CASTILLO
@@ -47,7 +47,7 @@ exports[`EcrSummary should match snapshot 1`] = ` DOB
04/15/2015
@@ -66,7 +66,7 @@ exports[`EcrSummary should match snapshot 1`] = ` Sex
male
@@ -85,7 +85,7 @@ exports[`EcrSummary should match snapshot 1`] = ` Patient Address
1050 CARPENTER ST EDWARDS, CA @@ -106,7 +106,7 @@ EDWARDS, CA Patient Contact
Home (818)419-5968 MELLY.C.A.16@GMAIL.COM @@ -140,7 +140,7 @@ MELLY.C.A.16@GMAIL.COM Facility Name
PRM- Palmdale Regional Medical Center
@@ -159,7 +159,7 @@ MELLY.C.A.16@GMAIL.COM Facility Contact
(661)382-5000
@@ -178,7 +178,7 @@ MELLY.C.A.16@GMAIL.COM Encounter Date/Time
Start: 05/13/2022 7:25 AM UTC End: 05/13/2022 9:57 AM UTC @@ -198,7 +198,7 @@ End: 05/13/2022 9:57 AM UTC Encounter Type
Emergency
@@ -263,7 +263,7 @@ End: 05/13/2022 9:57 AM UTC RCKMS Rule Summary
covid summary
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Encounter.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Encounter.test.tsx.snap index a480d39d7..9924d340f 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Encounter.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Encounter.test.tsx.snap @@ -31,7 +31,7 @@ exports[`Encounter should match snapshot 1`] = ` Encounter Type
Ambulatory
@@ -50,7 +50,7 @@ exports[`Encounter should match snapshot 1`] = ` Encounter ID
123456789
@@ -81,7 +81,7 @@ exports[`Encounter should match snapshot 1`] = ` Facility Name
PRM- Palmdale Regional Medical Center
@@ -100,7 +100,7 @@ exports[`Encounter should match snapshot 1`] = ` Facility Address
5001 North Mount Washington Circle Drive North Canton, MA 02740 @@ -120,7 +120,7 @@ North Canton, MA 02740 Facility Contact Address
5001 North Mount Washington Circle Drive North Canton, MA 02740 @@ -140,7 +140,7 @@ North Canton, MA 02740 Facility Type
Healthcare Provider
@@ -159,7 +159,7 @@ North Canton, MA 02740 Facility ID
2.16.840.1.113883.4.6
@@ -192,7 +192,7 @@ North Canton, MA 02740 Provider Name
test provider name
@@ -211,7 +211,7 @@ North Canton, MA 02740 Provider Contact
test provider contact
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap index eb49100b3..78645cf1e 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap @@ -80,24 +80,36 @@ exports[`LabInfo when labResults is DisplayDataProps[] should match snapshot tes - SARS-CoV-2, NAA CL + + SARS-CoV-2, NAA CL + - POS + + POS + - 2022-04-21T21:02:00.000Z + + 2022-04-21T21:02:00.000Z + - Symptomatic as defined by CDC? + + Symptomatic as defined by CDC? + - YES + + YES + - 2022-04-21T21:02:00.000Z + + 2022-04-21T21:02:00.000Z + @@ -146,7 +158,7 @@ exports[`LabInfo when labResults is LabReportElementData[] should match snapshot Lab Address
501 S. Buena Vista Street Burbank, CA @@ -167,7 +179,7 @@ Burbank, CA Number of Results
2
@@ -346,7 +358,7 @@ Burbank, CA Analysis Time
09/28/2022 2:00 PM PDT
@@ -365,7 +377,7 @@ Burbank, CA Collection Time
09/28/2022 8:51 PM UTC
@@ -384,7 +396,7 @@ Burbank, CA Received Time
09/28/2022 8:51 PM UTC
@@ -403,7 +415,7 @@ Burbank, CA Specimen (Source)
Stool
@@ -422,7 +434,7 @@ Burbank, CA Anatomical Location/Laterality
STOOL SPECIMEN / Unknown
@@ -441,7 +453,7 @@ Burbank, CA Collection Method/Volume
Ambhp1 Test MD
@@ -506,7 +518,7 @@ Burbank, CA Result Type
MICROBIOLOGY - GENERAL ORDERABLES
@@ -525,7 +537,7 @@ Burbank, CA Narrative
- 09/28/2022 1:59 PM PDT -
+ class="grid-col maxw7 text-pre-line p-list lab-text-content" + />
09/28/2022 8:51 PM UTC
@@ -714,7 +724,7 @@ Burbank, CA Received Time
09/28/2022 8:51 PM UTC
@@ -733,7 +743,7 @@ Burbank, CA Specimen (Source)
Stool
@@ -752,7 +762,7 @@ Burbank, CA Anatomical Location/Laterality
STOOL SPECIMEN / Unknown
@@ -771,7 +781,7 @@ Burbank, CA Collection Method/Volume
Ambhp1 Test MD
@@ -836,7 +846,7 @@ Burbank, CA Result Type
MICROBIOLOGY - GENERAL ORDERABLES
@@ -855,7 +865,7 @@ Burbank, CA Narrative
VUMC CERNER LAB
@@ -911,7 +921,7 @@ Burbank, CA Lab Address
4605 TVC VUMC 1301 Medical Center Drive @@ -933,7 +943,7 @@ NASHVILLE, TN Number of Results
1
@@ -1237,7 +1247,7 @@ NASHVILLE, TN Analysis Time
04/04/2022 4:55 PM UTC
@@ -1279,7 +1289,7 @@ NASHVILLE, TN Received Time
04/18/2022 7:46 PM UTC
@@ -1298,7 +1308,7 @@ NASHVILLE, TN Specimen (Source)
Blood
@@ -1317,7 +1327,7 @@ NASHVILLE, TN Anatomical Location/Laterality
Collection / Unknown
@@ -1359,9 +1369,11 @@ NASHVILLE, TN Resulting Agency Comment
- testing comment +

+ testing comment +

Carda Testprovqa MD
@@ -1397,7 +1409,7 @@ NASHVILLE, TN Result Type
LAB GENETIC TESTING
@@ -1416,9 +1428,14 @@ NASHVILLE, TN Narrative
- VUMC CERNER LAB - 04/18/2022 2:57 PM CDT MICROARRAY REPORT NARRATIVE +

+ VUMC CERNER LAB - 04/18/2022 2:57 PM CDT +

+

+ MICROARRAY REPORT NARRATIVE +

Nursing, psychiatric, and home health aides
@@ -49,7 +49,7 @@ exports[`SocialHistory should match snapshot 1`] = ` Tobacco Use
Tobacco smoking consumption unknown
@@ -68,7 +68,7 @@ exports[`SocialHistory should match snapshot 1`] = ` Homeless Status
unsatisfactory living conditions (finding)
@@ -87,7 +87,7 @@ exports[`SocialHistory should match snapshot 1`] = ` Occupation
Nursing, psychiatric, and home health aides
@@ -106,7 +106,7 @@ exports[`SocialHistory should match snapshot 1`] = ` Sexual Orientation
Do not know
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Unavailable.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Unavailable.test.tsx.snap index 9ec70b10e..f32aa83ba 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Unavailable.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Unavailable.test.tsx.snap @@ -32,7 +32,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Tribal Affiliation
No data
@@ -51,7 +51,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Preffered Language
No data
@@ -83,7 +83,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Travel History
No data
@@ -102,7 +102,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Pregnancy Status
No data
@@ -121,7 +121,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Alcohol Use
No data
@@ -140,7 +140,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Sexual Orientation
No data
@@ -159,7 +159,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Gender Identity
No data
@@ -191,7 +191,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Encounter Date/Time
No data
@@ -210,7 +210,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Encounter Type
No data
@@ -229,7 +229,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Encounter ID
No data
@@ -261,7 +261,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Facility Name
No data
@@ -280,7 +280,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Facility Address
No data
@@ -299,7 +299,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Facility Contact Address
No data
@@ -318,7 +318,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Facility Type
No data
@@ -337,7 +337,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Facility ID
No data
@@ -369,7 +369,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Miscellaneous Notes
No data
@@ -401,7 +401,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Provider Facility Name
No data
@@ -420,7 +420,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Provider Facility Address
No data
@@ -452,7 +452,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Reason for Visit
No data
@@ -471,7 +471,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Problems List
No data
@@ -503,7 +503,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Vitals
No data
@@ -535,7 +535,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Immunization History
No data
@@ -567,7 +567,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Procedures
No data
@@ -599,7 +599,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` EHR Software Name
No data
@@ -618,7 +618,7 @@ exports[`UnavailableInfo should match snapshot 1`] = ` Custodian Contact
No data
diff --git a/containers/ecr-viewer/src/app/tests/services/__snapshots__/labsService.test.tsx.snap b/containers/ecr-viewer/src/app/tests/services/__snapshots__/labsService.test.tsx.snap new file mode 100644 index 000000000..2d4759eb4 --- /dev/null +++ b/containers/ecr-viewer/src/app/tests/services/__snapshots__/labsService.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Labs Utils returnFieldValueFromLabHtmlString extracts correct field value from within a lab report 1`] = ` + +

+ + 09/28/2022 1:59:00 PM PDT + +

+

+ 09/28/2022 1:59:00 PM PDT +

+
+`; diff --git a/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx b/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx index 2f7491f06..569295705 100644 --- a/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx +++ b/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx @@ -47,11 +47,15 @@ const labReportNormalJsonObject = { Value: { value: "Not Detected", metadata: {} }, "Ref Range": { value: "Not Detected", metadata: {} }, "Test Method": { - value: "LAB DEVICE: BIOFIRE® FILMARRAY® 2.0 SYSTEM", + value:

LAB DEVICE: BIOFIRE® FILMARRAY® 2.0 SYSTEM

, metadata: {}, }, "Analysis Time": { - value: "09/28/2022 1:59 PM PDT", + value: ( +

+ 09/28/2022 1:59:00 PM PDT +

+ ), metadata: {}, }, "Performed At": { @@ -78,11 +82,11 @@ const labReportNormalJsonObject = { Value: { value: "Not Detected", metadata: {} }, "Ref Range": { value: "Not Detected", metadata: {} }, "Test Method": { - value: "LAB DEVICE: BIOFIRE® FILMARRAY® 2.0 SYSTEM", + value:

LAB DEVICE: BIOFIRE® FILMARRAY® 2.0 SYSTEM

, metadata: {}, }, "Analysis Time": { - value: "09/28/2022 1:59 PM PDT", + value:

09/28/2022 1:59:00 PM PDT

, metadata: {}, }, "Performed At": { @@ -135,19 +139,22 @@ const labReportNormalJsonObject = { [ { "Performing Organization": { - value: - "PROVIDENCE ST. JOSEPH MEDICAL CENTER LABORATORY (CLIA 05D0672675)", + value: ( +

+ PROVIDENCE ST. JOSEPH MEDICAL CENTER LABORATORY (CLIA 05D0672675) +

+ ), metadata: { "data-id": "Result.1.2.840.114350.1.13.297.3.7.2.798268.1670845.PerformingLab", }, }, - Address: { value: "501 S. Buena Vista Street", metadata: {} }, + Address: { value:

501 S. Buena Vista Street

, metadata: {} }, "City/State/ZIP Code": { - value: "Burbank, CA 91505", + value:

Burbank, CA 91505

, metadata: {}, }, - "Phone Number": { value: "818-847-6000", metadata: {} }, + "Phone Number": { value:

818-847-6000

, metadata: {} }, }, ], ], @@ -237,29 +244,29 @@ describe("Labs Utils", () => { { "Lab Test Name": { metadata: {}, - value: "SARS-CoV-2, NAA CL", + value: SARS-CoV-2, NAA CL, }, "Lab Test Result Date": { metadata: {}, - value: "2022-04-21T21:02:00.000Z", + value: 2022-04-21T21:02:00.000Z, }, "Lab Test Result Value": { metadata: {}, - value: "POS", + value: POS, }, }, { "Lab Test Name": { metadata: {}, - value: "Symptomatic as defined by CDC?", + value: Symptomatic as defined by CDC?, }, "Lab Test Result Date": { metadata: {}, - value: "2022-04-21T21:02:00.000Z", + value: 2022-04-21T21:02:00.000Z, }, "Lab Test Result Value": { metadata: {}, - value: "YES", + value: YES, }, }, ], @@ -311,7 +318,7 @@ describe("Labs Utils", () => { const result = searchResultRecord(labHTMLJson, searchKey); - expect(result).toBe(expectedResult); + expect(result).toStrictEqual(expectedResult); }); it("returns an empty string of results if none are found for search key", () => { @@ -320,21 +327,20 @@ describe("Labs Utils", () => { const result = searchResultRecord(labHTMLJson, invalidSearchKey); - expect(result).toBe(expectedResult); + expect(result).toStrictEqual(expectedResult); }); }); describe("returnFieldValueFromLabHtmlString", () => { it("extracts correct field value from within a lab report", () => { const fieldName = "Analysis Time"; - const expectedResult = "09/28/2022 1:59 PM PDT"; const result = returnFieldValueFromLabHtmlString( labReportNormalJsonObject, fieldName, ); - expect(result).toBe(expectedResult); + expect(result).toMatchSnapshot(); }); it("returns NoData if none are found for field name", () => { diff --git a/containers/ecr-viewer/src/app/tests/utils.test.tsx b/containers/ecr-viewer/src/app/tests/utils.test.tsx index 643230600..635a49cda 100644 --- a/containers/ecr-viewer/src/app/tests/utils.test.tsx +++ b/containers/ecr-viewer/src/app/tests/utils.test.tsx @@ -1,4 +1,4 @@ -import { isDataAvailable, sanitizeAndMap } from "@/app/view-data/utils/utils"; +import { isDataAvailable, safeParse } from "@/app/view-data/utils/utils"; import { loadYamlConfig } from "@/app/api/utils"; import { Bundle } from "fhir/r4"; import BundleWithTravelHistory from "./assets/BundleTravelHistory.json"; @@ -11,7 +11,7 @@ import BundleWithScheduledOrdersOnly from "./assets/BundleScheduledOrdersOnly.js import BundleNoActiveProblems from "./assets/BundleNoActiveProblems.json"; import BundleCareTeam from "./assets/BundleCareTeam.json"; import React from "react"; -import { render, screen } from "@testing-library/react"; +import { render, screen, cleanup } from "@testing-library/react"; import { CarePlanActivity } from "fhir/r4b"; import { evaluate } from "@/app/view-data/utils/evaluate"; import userEvent from "@testing-library/user-event"; @@ -551,19 +551,42 @@ describe("Utils", () => { }); }); - describe("sanitizeAndMap", () => { + describe("safeParse", () => { + it("should leave a string safe HTML", () => { + const str = "hi there"; + const actual = safeParse(str); + expect(actual).toBe(str); + }); + it("should leave alone nice safe HTML", () => { - const html = "

hi there

"; - const actual = sanitizeAndMap(html); - expect(actual).toBe(html); + const str = "

hi there

"; + const jsx =

hi there

; + const actual = safeParse(str); + expect(actual).toStrictEqual(jsx); + }); + + it("should remove empty nodes", () => { + const str = `


hiya`; + const parsed = safeParse(str); + const { asFragment } = render(parsed); + expect(asFragment()).toMatchSnapshot(); + cleanup(); }); it("should map xml-y HTML", () => { - const html = `hi thereI'm contentonetwo`; - const actual = sanitizeAndMap(html); - expect(actual).toBe( - `

hi there

I'm content`, - ); + const str = `hi thereI'm contentonetwo`; + const parsed = safeParse(str); + const { asFragment } = render(parsed); + expect(asFragment()).toMatchSnapshot(); + cleanup(); + }); + + it("should remove comments", () => { + const str = `hi thereI'm contentonetwo`; + const parsed = safeParse(str); + const { asFragment } = render(parsed); + expect(asFragment()).toMatchSnapshot(); + cleanup(); }); }); }); diff --git a/containers/ecr-viewer/src/app/view-data/components/DataDisplay.tsx b/containers/ecr-viewer/src/app/view-data/components/DataDisplay.tsx index 565a7a7aa..5b808cf59 100644 --- a/containers/ecr-viewer/src/app/view-data/components/DataDisplay.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/DataDisplay.tsx @@ -7,7 +7,7 @@ export interface DisplayDataProps { title?: string; className?: string; toolTip?: string; - value?: string | React.JSX.Element | React.JSX.Element[] | React.ReactNode; + value?: React.ReactNode; dividerLine?: boolean; table?: boolean; } @@ -41,7 +41,7 @@ export const DataDisplay: React.FC<{
parse(sanitizeAndMap(v)), + applyToValue: (v) => safeParse(v), hiddenBaseText: "comment", }, ]; @@ -518,10 +517,8 @@ export const evaluateClinicalData = ( const clinicalNotes: DisplayDataProps[] = [ { title: "Miscellaneous Notes", - value: parse( - sanitizeAndMap( - evaluateValue(fhirBundle, mappings["historyOfPresentIllness"]), - ) || "", + value: safeParse( + evaluateValue(fhirBundle, mappings["historyOfPresentIllness"]) ?? "", ), }, ]; diff --git a/containers/ecr-viewer/src/app/view-data/utils/utils.tsx b/containers/ecr-viewer/src/app/view-data/utils/utils.tsx index ffa27bb18..ab409e71e 100644 --- a/containers/ecr-viewer/src/app/view-data/utils/utils.tsx +++ b/containers/ecr-viewer/src/app/view-data/utils/utils.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { Fragment } from "react"; import { removeHtmlElements } from "@/app/services/formatService"; import { DisplayDataProps } from "@/app/view-data/components/DataDisplay"; import sanitizeHtml from "sanitize-html"; +import parse from "html-react-parser"; export interface PathMappings { [key: string]: string; @@ -12,6 +13,8 @@ export interface CompleteData { unavailableData: DisplayDataProps[]; } +export type RenderableNode = string | React.JSX.Element; + export const noData = ( No data ); @@ -60,17 +63,73 @@ export const isDataAvailable = (item: DisplayDataProps): Boolean => { }; /** - * Sanitizes the html while also mapping common hl7v3 tags to html. - * @param val - The string of content to sanitize and map. + * Parses and sanitizes the html while also mapping common hl7v3 tags to html. + * @param val - The string of content to parse. * @returns - Returns sanitized and mapped content. */ -export const sanitizeAndMap = (val: string): string => { - return sanitizeHtml(val, { - transformTags: { - paragraph: "p", - list: "ul", - item: "li", - content: "span", - }, - }); +export const safeParse = (val: string): RenderableNode => { + const parsed = parse( + sanitizeHtml(val, { + transformTags: { + paragraph: "p", + list: "ul", + item: "li", + content: "span", + }, + }), + ); + + return Array.isArray(parsed) ? arrayToElement(parsed) : parsed; +}; + +/** + * Collapses an array of values into one element. + * @param vals - An array of strings and elments. + * @returns - One string or element. + */ +export const arrayToElement = (vals: RenderableNode[]) => { + // Filter out empty nodes. + const trimmed = vals.map(trimEmptyElements).filter(Boolean); + + // An empty array is returned for an empty input + if (trimmed.length === 0) { + return ""; + } else if (trimmed.length === 1) { + return vals[0]; + } + + // Wrap the items in a fragment and make sure they all have keys. + // It reduces the cases to handle elsewhere in the logic and avoids + // duplicate key warnings. + return ( + <> + {trimmed.map((item, ind) => { + // Make sure items always have a key + const key = `el-${ind}`; + return typeof item !== "object" ? ( + {item} + ) : ( + { ...item, key } + ); + })} + + ); +}; + +/** + * Return "" if an element is empty, otherwise return the element. + *
are returned as is as it shouldn't have content in it. + * @param val - A RenderableNode. + * @returns - A string or element. + */ +const trimEmptyElements = (val: RenderableNode) => { + if (typeof val === "string") { + return val.trim(); + } + + // allowed to be empty - self-closing + if (val.type == "br") return val; + + // got children? go ahead + return val.props.children ? val : ""; };