Skip to content

Commit

Permalink
Form display control UI in react (#686)
Browse files Browse the repository at this point in the history
* Added mf for forms display control
* Add Sorting
* Added edit button and css fix
* Edit button visibility based on config, Fix CSS
* Add translation, test suite and css fix
* Included numberOfVisits config
* Fix active encounter issue, test issue
* Updated test-suite date format using moment
* Update import name fix
  • Loading branch information
SooryaKumaranC-tw authored Sep 4, 2023
1 parent d1979a1 commit afe7c2e
Show file tree
Hide file tree
Showing 23 changed files with 643 additions and 19 deletions.
19 changes: 11 additions & 8 deletions micro-frontends/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,39 @@
"@babel/preset-env": "^7.22.4",
"@babel/preset-react": "^7.22.3",
"@testing-library/react": "12.1.5",
"babel-loader": "^9.1.2",
"axios": "1.4.0",
"babel-jest": "^29.5.0",
"babel-loader": "^9.1.2",
"css-loader": "5.2.6",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-fetch-mock": "^3.0.3",
"mini-css-extract-plugin": "^2.7.6",
"prettier": "^2.8.8",
"resolve-url-loader": "^3.1.0",
"sass": "^1.63.4",
"sass-loader": "10.1.0",
"webpack": "^5.86.0",
"webpack-cli": "^5.1.4",
"css-loader": "5.2.6",
"sass": "^1.63.4",
"sass-loader": "10.1.0"
"copy-webpack-plugin": "^11.0.0"
},
"dependencies": {
"@carbon/icons-react": "^10.18.0",
"angular-component": "^0.1.3",
"react2angular": "^4.0.6",
"bahmni-carbon-ui": "0.1.3",
"classnames": "2.3.1",
"carbon-components": "^10.19.0",
"carbon-components-react": "^7.25.0",
"classnames": "2.3.1",
"moment": "^2.29.4",
"prop-types": "^15.7.2",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-intl": "^3.3.2"
"react-intl": "^3.3.2",
"react2angular": "^4.0.6"
},
"peerDependencies": {
"react-dom": "16.14.0",
Expand Down
5 changes: 5 additions & 0 deletions micro-frontends/public/i18n/locale_en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"NO_FORM": "No Form found for this patient....",
"DASHBOARD_TITLE_FORMS_2_DISPLAY_CONTROL_KEY": "Observation Forms",
"LOADING_MESSAGE": "Loading... Please Wait"
}
27 changes: 27 additions & 0 deletions micro-frontends/src/next-ui/Components/i18n/I18nProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import PropTypes from "prop-types";
import React, { useEffect, useMemo, useState } from "react";
import { IntlProvider } from "react-intl";
import { getLocale, getTranslations } from "./utils";

export function I18nProvider({ children }) {
const [messages, setMessages] = useState(undefined);
const locale = useMemo(getLocale, []);

useEffect(() => {
getTranslations(locale).then(setMessages);
}, []);

if (!messages) {
return <div></div>;
}

return (
<IntlProvider defaultLocale="en" locale={locale} messages={messages}>
{children}
</IntlProvider>
);
}

I18nProvider.propTypes = {
children: PropTypes.node.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getTranslations() {
return import("../../../../../public/i18n/locale_en.json").then(
(module) => module.default
);
}

export const getLocale = () => "en";
18 changes: 18 additions & 0 deletions micro-frontends/src/next-ui/Components/i18n/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { LS_LANG_KEY, BASE_URL } from "../../constants";

const translationsBaseUrl = "i18n";

export function getLocale() {
return localStorage.getItem(LS_LANG_KEY) || "en";;
}

export const getTranslations = async (locale) => {
const fileName = `locale_${locale}.json`;
return fetchTranslations(fileName);
};

async function fetchTranslations(fileName) {
const url = `${BASE_URL}${translationsBaseUrl}/${fileName}`;
const response = await fetch(url);
return response.json();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Accordion, AccordionItem } from "carbon-components-react";
import "../../../styles/carbon-conflict-fixes.scss";
import "../../../styles/carbon-theme.scss";
import "../../../styles/common.scss";
import "./formDisplayControl.scss";
import { FormattedMessage } from "react-intl";
import { fetchFormData } from "../../utils/FormDisplayControl/FormUtils";
import moment from "moment";
import { I18nProvider } from '../../Components/i18n/I18nProvider';
/** NOTE: for reasons known only to react2angular,
* any functions passed in as props will be undefined at the start, even ones inside other objects
* so you need to use the conditional operator like props.hostApi?.callback even though it is a mandatory prop
*/

export function FormDisplayControl(props) {
const noFormText = <FormattedMessage id={'NO_FORM'} defaultMessage={'No Form found for this patient....'} />;
const formsHeading = <FormattedMessage id={'DASHBOARD_TITLE_FORMS_2_DISPLAY_CONTROL_KEY'} defaultMessage={'Observation Forms'} />;
const loadingMessage = <FormattedMessage id={'LOADING_MESSAGE'} defaultMessage={'Loading... Please Wait'} />;

const [formList, setFormList] = useState([]);
const [isLoading, setLoading] = useState(true);
const buildResponseData = async () => {
try {
const formResponseData = await fetchFormData(props?.hostData?.patientUuid, props?.hostData?.numberOfVisits);
var grouped = {};
if (formResponseData?.length > 0) {
formResponseData.forEach(function (formEntry) {
grouped[formEntry.formName] = grouped[formEntry.formName] || [];
grouped[formEntry.formName].push({
encounterDate: formEntry.encounterDateTime,
encounterUuid: formEntry.encounterUuid,
visitUuid: formEntry.visitUuid,
visitDate: formEntry.visitStartDateTime,
providerName: formEntry.providers[0].providerName,
providerUuid: formEntry.providers[0].uuid
});
});
}
Object.keys(grouped).forEach(function (key) {
grouped[key] = grouped[key].sort((a, b) => {
return new Date(b.encounterDate) - new Date(a.encounterDate);
});
})
setFormList(grouped);
} catch (e) {
console.log(e);
} finally {
setLoading(false);
}
};

const showEdit = function (currentEncounterUUid) {
return props?.hostData?.showEditForActiveEncounter ? (props?.hostData?.encounterUuid === currentEncounterUUid) : true;
}

useEffect(() => {
buildResponseData();
}, []);

return (
<>
<I18nProvider>
<div>
<h2 className={"section-title"}>
{formsHeading}
</h2>
{isLoading ? <div className="loading-message">{loadingMessage}</div> : (
<div className={"placeholder-text"}>{Object.entries(formList).length > 0 ? (
Object.entries(formList).map(([key, value]) => {
const moreThanOneEntry = value.length > 1;
return (
moreThanOneEntry ? (<Accordion>
<AccordionItem title={key} className={"form-accordion"} open>
{
value.map((entry) => {
return (
<div className={"row-accordion"}>
<span className={"form-name-text"}><a className="form-link">{moment(entry.encounterDate).format("DD/MM/YYYY HH:MM")}</a>{showEdit(entry.encounterUuid) && <i className="fa fa-pencil"></i>}</span>
<span className={"form-provider-text"}>{entry.providerName}</span>
</div>
);
})
}
</AccordionItem>
</Accordion>) :
<div className={"row"}>
<span className={"form-non-accordion-text form-heading"}>{key}</span>
<span className={"form-non-accordion-text form-date-align"}><a className="form-link">{moment(value[0].encounterDate).format("DD/MM/YYYY HH:MM")}</a>{showEdit(value[0].encounterUuid) && <i className="fa fa-pencil"></i>}</span>
<span className={"form-non-accordion-text"}>{value[0].providerName}</span>
</div>
);
}))
: (noFormText)}
</div>

)}

</div>
</I18nProvider>
</>
);
}

FormDisplayControl.propTypes = {
hostData: PropTypes.object.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React from "react";
import { fireEvent, render, screen, waitFor, mount } from "@testing-library/react";
import { FormDisplayControl } from "./FormDisplayControl";
import { mockFormResponseData } from "./FormDisplayControlMockData";
import moment from "moment";

const mockFetchFormData = jest.fn();

jest.mock("../../utils/FormDisplayControl/FormUtils", () => ({
fetchFormData: () => mockFetchFormData(),
}));

jest.mock("../../Components/i18n/I18nProvider", () => ({
I18nProvider: ({ children }) => <div>{children}</div>
}));

const mockHostData = {
patientUuid: 'some-patient-uuid',
showEditForActiveEncounter: true,
encounterUuid: 'some-encounter-uuid'
};

describe('FormDisplayControl Component for empty mock data', () => {
it('should show no-forms-message when form entries are empty', async () => {
const mockWithPatientHostData = {
patientUuid: 'some-patient-uuid',
encounterUuid: undefined
};
mockFetchFormData.mockResolvedValueOnce({});

const { container } = render(<FormDisplayControl hostData={mockWithPatientHostData} />);

await waitFor(() => {
// expect(screen.getByText('No Form found for this patient....')).toBeTruthy();
expect(container.querySelector(".placeholder-text").innerHTML).toEqual('No Form found for this patient....');
});
});
});

describe('FormDisplayControl Component', () => {

it("should render the component", () => {
const { container } = render(<FormDisplayControl hostData={mockHostData} />);
expect(container).toMatchSnapshot();
});

it('should show loading message', () => {
const { container } = render(<FormDisplayControl hostData={mockHostData} />);
expect(container.querySelector('.loading-message')).not.toBeNull();
expect(container.querySelector('.loading-message').innerHTML).toEqual('Loading... Please Wait');
});

});

;
describe('FormDisplayControl Component with Accordion and Non-Accordion', () => {

beforeEach(() => {
mockFetchFormData.mockResolvedValue(mockFormResponseData);
});
// it("should render the component with form data", async() => {
// mockFetchFormData.mockResolvedValueOnce(mockFormResponseData);
// const { container } = render(<FormDisplayControl hostData={mockHostData} />);
// await waitFor(() => {
// expect(container).toMatchSnapshot();
// });
// });

it('should render accordion form entries when loading is done', async () => {
const { container } = render(<FormDisplayControl hostData={mockHostData} />);

await waitFor(() => {
expect(container.querySelectorAll(".bx--accordion__title")).toHaveLength(1);
expect(container.querySelector(".bx--accordion__title").innerHTML).toEqual('Pre Anaesthesia Assessment');
expect(container.querySelector(".row-accordion > .form-name-text > .form-link").innerHTML).toEqual(moment(1693217959000).format("DD/MM/YYYY HH:MM"));
expect(container.querySelector(".row-accordion > .form-provider-text").innerHTML).toEqual('Doctor One');

});
});

it('should render non-accordion form entries when loading is done', async () => {
const { container } = render(<FormDisplayControl hostData={mockHostData} />);

await waitFor(() => {
expect(container.querySelectorAll(".form-non-accordion-text")).toHaveLength(6);
expect(container.querySelectorAll(".form-non-accordion-text.form-heading")[0].innerHTML).toEqual('Orthopaedic Triage');
expect(container.querySelectorAll(".form-non-accordion-text.form-date-align > a")[0].innerHTML).toEqual(moment(1693277657000).format("DD/MM/YYYY HH:MM"));
expect(container.querySelectorAll(".form-non-accordion-text")[2].innerHTML).toEqual('Doctor Two');
expect(container.querySelectorAll(".form-non-accordion-text.form-heading")[1].innerHTML).toEqual('Patient Progress Notes and Orders');
expect(container.querySelectorAll(".form-non-accordion-text.form-date-align > a")[1].innerHTML).toEqual(moment(1693277657000).format("DD/MM/YYYY HH:MM"));
expect(container.querySelectorAll(".form-non-accordion-text")[5].innerHTML).toEqual('Doctor One');
});

});

it('should not see edit button for non-active-encounter entries and when showEditForActiveEncounter is true', async () => {
const { container } = render(<FormDisplayControl hostData={mockHostData} />);

await waitFor(() => {
expect(container.querySelectorAll(".fa.fa-pencil")).toHaveLength(0);
});
});

it('should see edit button for active-encounter entries and when showEditForActiveEncounter is true', async () => {
const activeEncounterMockHostData = {
patientUuid: 'some-patient-uuid',
showEditForActiveEncounter: true,
encounterUuid: '6e52cecd-a095-457f-9515-38cf9178cb50'
};
const { container } = render(<FormDisplayControl hostData={activeEncounterMockHostData} />);

await waitFor(() => {
expect(container.querySelectorAll(".fa.fa-pencil")).toHaveLength(2);
});
});

it('should see edit button for all entries and when showEditForActiveEncounter is false', async () => {
const activeEncounterMockHostData = {
patientUuid: 'some-patient-uuid',
showEditForActiveEncounter: false,
encounterUuid: '6e52cecd-a095-457f-9515-38cf9178cb50'
};
const { container } = render(<FormDisplayControl hostData={activeEncounterMockHostData} />);

await waitFor(() => {
expect(container.querySelectorAll(".fa.fa-pencil")).toHaveLength(4);
});
});

it('should see edit button for all entries and when showEditForActiveEncounter is not present', async () => {
const activeEncounterMockHostData = {
patientUuid: 'some-patient-uuid',
encounterUuid: '6e52cecd-a095-457f-9515-38cf9178cb50'
};
const { container } = render(<FormDisplayControl hostData={activeEncounterMockHostData} />);

await waitFor(() => {
expect(container.querySelectorAll(".fa.fa-pencil")).toHaveLength(4);
});
});

});
Loading

0 comments on commit afe7c2e

Please sign in to comment.