-
Notifications
You must be signed in to change notification settings - Fork 290
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Form display control UI in react (#686)
* 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
1 parent
d1979a1
commit afe7c2e
Showing
23 changed files
with
643 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
27
micro-frontends/src/next-ui/Components/i18n/I18nProvider.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
7 changes: 7 additions & 0 deletions
7
micro-frontends/src/next-ui/Components/i18n/__mocks__/utils.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
108 changes: 108 additions & 0 deletions
108
micro-frontends/src/next-ui/Containers/formDisplayControl/FormDisplayControl.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
142 changes: 142 additions & 0 deletions
142
micro-frontends/src/next-ui/Containers/formDisplayControl/FormDisplayControl.spec.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
|
||
}); |
Oops, something went wrong.