Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: lab value display #2931

Merged
merged 16 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
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 @@ -375,7 +373,7 @@ export function formatTablesToJSON(htmlString: string): TableJson[] {
const tableWithCaptionArray = doc.querySelectorAll("table:has(caption)");
if (tableWithCaptionArray.length > 0) {
doc.querySelectorAll("table").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 @@ -387,7 +385,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 @@ -457,7 +455,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 @@ -489,8 +487,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
75 changes: 42 additions & 33 deletions containers/ecr-viewer/src/app/services/labsService.tsx
BobanL marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -119,33 +120,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 @@ -159,7 +158,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 @@ -181,7 +180,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 @@ -206,7 +205,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 @@ -229,14 +228,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 @@ -252,20 +251,30 @@ 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 (typeof el?.props?.children === "string") {
dateTime = el.props.children;
} else {
return "";
}
return formatDateTime(dateTime);
});

return [...new Set(analysisTimeArrayFormatted)].join(", ");
return [...new Set(analysisTimeArrayFormatted.filter(Boolean))].join(", ");
};

/**
Expand Down Expand Up @@ -341,15 +350,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,37 @@
// 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>
`;
Loading
Loading