Skip to content

Commit

Permalink
fix: lab value display (#2931)
Browse files Browse the repository at this point in the history
* fix: lab value display

* fix: fold in parsing with sanitizing, add keys, remove arrs

* refactor: always deal witha single elements

* fix: linting

* refactor: TableValue => RenderableNode

* fix: add p-list class

* fix: missing []

* Update containers/ecr-viewer/src/app/view-data/utils/utils.tsx

Co-authored-by: Boban <BobanL@users.noreply.github.com>

* Update containers/ecr-viewer/src/app/view-data/utils/utils.tsx

Co-authored-by: Boban <BobanL@users.noreply.github.com>

* fix: handle empty nodes

* Update containers/ecr-viewer/seed-scripts/create-seed-data.py

Co-authored-by: Boban <BobanL@users.noreply.github.com>

* test: update tests post-merge

* fix: handling of nested time values

---------

Co-authored-by: Boban <BobanL@users.noreply.github.com>
  • Loading branch information
mcmcgrath13 and BobanL authored Nov 22, 2024
1 parent 8691ed2 commit c4b741e
Show file tree
Hide file tree
Showing 20 changed files with 481 additions and 286 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
38 changes: 29 additions & 9 deletions containers/ecr-viewer/src/app/services/formatService.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
});
Expand All @@ -377,7 +375,7 @@ export function formatTablesToJSON(htmlString: string): TableJson[] {
) as NodeListOf<HTMLTableElement>;
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)] });
});
Expand All @@ -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;

Expand Down Expand Up @@ -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));
});
}

Expand Down Expand Up @@ -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 - <paragraph><!-- comment -->Values <content>here</content></paragraph>
* @example @returns - <p>Values <span>here</span></p>
*/
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 - <paragraph><!-- comment -->Values <content>here</content></paragraph>
* @example @returns - 'Values here'
*/
function getElementText(el: Element | Node): string {
return el.textContent?.trim() ?? "";
}

/**
Expand Down
78 changes: 45 additions & 33 deletions containers/ecr-viewer/src/app/services/labsService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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"]);
Expand All @@ -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"]);
Expand All @@ -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"]);
Expand All @@ -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;
}

Expand All @@ -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(", ");
};

/**
Expand Down Expand Up @@ -371,15 +383,15 @@ 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",
},
{
columnName: "Lab Comment",
infoPath: "observationNote",
hiddenBaseText: "comment",
applyToValue: (v) => parse(sanitizeAndMap(v)),
applyToValue: (v) => safeParse(v),
className: "minw-10 width-20",
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Utils safeParse should map xml-y HTML 1`] = `
<DocumentFragment>
<p>
hi there
</p>
<span>
I'm content
</span>
<ul>
<li>
one
</li>
<li>
two
</li>
</ul>
</DocumentFragment>
`;

exports[`Utils safeParse should remove comments 1`] = `
<DocumentFragment>
<p>
hi there
</p>
I'm content
<ul>
<li>
one
</li>
<li>
two
</li>
</ul>
</DocumentFragment>
`;

exports[`Utils safeParse should remove empty nodes 1`] = `
<DocumentFragment>
<br />
<span>
hiya
</span>
</DocumentFragment>
`;
2 changes: 1 addition & 1 deletion containers/ecr-viewer/src/app/tests/assets/BundleLab.json

Large diffs are not rendered by default.

Loading

0 comments on commit c4b741e

Please sign in to comment.