Adding a left panel
The left panel (opens in a new tab) provides navigation within an App in O3. It is based on Carbon's UI shell left panel (opens in a new tab), and is positioned below the header, fixed to the left edge of the page. It is possible to add a left panel to any page in O3 if you need to leverage its navigation capabilities.
This guide will walk you through the process of adding a left panel to the Bed Management app (opens in a new tab), which is a frontend module that's part of the UgandaEMR+ instance. The bed management app handles the management of beds in a hospital, and is a good candidate for a left panel because it has multiple pages that need to be navigated to.
Example: Adding a left panel to the Bed Management app
The Bed Management app has the following screens that need to be navigated to:
- The landing screen, which shows a summary of the number of beds in each ward.
- A detail screen for a specific ward, which shows the number of beds in that ward and their current status.
- A ward allocation screen, which allows you to add, edit and delete beds, as well as allocating beds to wards.
We'll need to setup the following navigation links in the left panel:
- A
Summary
link for the landing screen and the ward detail screen. - An
Administration
link for the bed management administration screen.
Below is a screenshot of how the app looks like:
To achieve this, we're going to follow the steps below:
Step 1: Set up the left panel
Begin by updating your root component (or whatever component your app uses to set up routing) as follows:
import React, { useEffect } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { LeftNavMenu, setLeftNav, unsetLeftNav } from "@openmrs/esm-framework";
import BedAdministrationTable from "./bed-administration/bed-administration-table.component";
import Home from "./home.component";
import WardWithBeds from "./ward-with-beds/ward-with-beds.component";
import styles from "./root.scss";
const Root: React.FC = () => {
const spaBasePath = window.spaBase;
useEffect(() => {
setLeftNav({
name: "bed-management-left-panel-slot",
basePath: spaBasePath,
});
return () => unsetLeftNav("bed-management-left-panel-slot");
}, [spaBasePath]);
return (
<BrowserRouter basename={`${window.getOpenmrsSpaBase()}bed-management`}>
<LeftNavMenu />
<main className={styles.container}>
<Routes>
<Route path="/summary" element={<Home />} />
<Route path="/ward/:wardUuid" element={<WardWithBeds />} />
<Route path="/ward-allocation" element={<BedAdministrationTable />} />
</Routes>
</main>
</BrowserRouter>
);
};
export default Root;
Some key things to note here are:
-
We're importing the
LeftNavMenu
component from the@openmrs/esm-framework
package. This component renders the left panel. We're also importing thesetLeftNav
andunsetLeftNav
functions from the same package. These functions are used to register and unregister the left panel with the LeftNav store (opens in a new tab) respectively. The LeftNav store is a Zustand (opens in a new tab) store that keeps track of all the left panels that have been registered in the app. -
We're calling the
setLeftNav
function in auseEffect
hook. This function takes an object with two properties:name
: The name of the slot that the left panel should be rendered in. This is the same name that you'll use when adding the left panel to theextensions
array of yourroutes.json
file.basePath
: The base path of the app (/openmrs/spa
by default). This is used to ensure that the left panel links are relative to the app's base path.
-
We're calling the
unsetLeftNav
function in auseEffect
cleanup function. This function takes the name of the slot that the left panel was rendered in. This ensures that the left panel gets unregistered when the component unmounts. -
We're rendering the
LeftNavMenu
component as the first child of theBrowserRouter
component. This ensures that the left panel gets rendersd in all the routes of the app. -
We're setting up three routes for the Bed Management app:
- A
/summary
route that renders theHome
component. - A
/ward/:wardUuid
route that renders theWardWithBeds
component. The:ward
portion of the route is a URL parameter that gets passed to theWardWithBeds
component as a prop. This component is used to render the detail page for a specific ward. - An
/ward-allocation
route that renders theBedAdministrationTable
component.
- A
Step 2: Wiring up the root page
Next, we'll create a named export for the Root
component inside the index.ts
file. This is the component that we'll use to wire up the Root
component to the bed-management
route so that it gets rendered when you navigate to that route.
Add the following to your index.ts
file:
import rootComponent from './root.component';
export const root = getSyncLifecycle(rootComponent), options);
Next, modify your routes.json
file to include the following page definition:
{
"$schema": "https://json.openmrs.org/routes.schema.json",
"backendDependencies": {
"fhir2": "^1.2.0",
"webservices.rest": "^2.24.0"
},
"pages": [
{
"component": "root",
"route": "bed-management"
}
]
}
This page definition tells O3 to render the Root
component when you navigate to the bed-management
route.
Step 3: Add links to the left panel
Next, we'll want to add the two links that we mentioned earlier to the left panel:
- A
Summary
link for the landing screen. - A
Ward allocation
link for the ward allocation screen.
To do this, we'll need to create two extensions that render the links. We'll then add those extensions to the extensions
array of our routes.json
file. The named exports of those extensions will be used as the component
property of the extensions, and look like the following:
export const summaryLeftPanelLink = getSyncLifecycle(
createLeftPanelLink({
name: "summary",
title: t("summary", "Summary"),
}),
options
);
export const wardAllocationLeftPanelLink = getSyncLifecycle(
createLeftPanelLink({
name: "ward-allocation",
title: t("wardAllocation", "Ward Allocation"),
}),
options
);
The createLeftPanelLink
function is a higher-order function that takes a name
and a title
property. The name
property is the unique path that the URL segment gets matched against. If the name matches the last portion of the URL, then the matching link gets some special styling to indicate that it is the active link. The title
property gets rendered as the link text.
We're importing this createLeftPanelLink
function from a separate file called left-panel-link.component.tsx
that looks like the following:
import React, { useMemo } from "react";
import last from "lodash-es/last";
import { BrowserRouter, useLocation } from "react-router-dom";
import { ConfigurableLink } from "@openmrs/esm-framework";
export interface LinkConfig {
name: string;
title: string;
}
function LinkExtension({ config }: { config: LinkConfig }) {
const { name, title } = config;
const location = useLocation();
let urlSegment = useMemo(() => decodeURIComponent(last(location.pathname.split("/"))), [location.pathname]);
const isUUID = (value) => {
const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
return regex.test(value);
};
if (isUUID(urlSegment)) {
urlSegment = "summary";
}
return (
<ConfigurableLink
to={`${window.getOpenmrsSpaBase()}bed-management${name ? `/${name}` : ""}`}
className={`cds--side-nav__link ${name === urlSegment && "active-left-nav-link"}`}
>
{title}
</ConfigurableLink>
);
}
export const createLeftPanelLink = (config: LinkConfig) => () =>
(
<BrowserRouter>
<LinkExtension config={config} />
</BrowserRouter>
);
Some key things to note here are:
- We're using the ConfigurableLink (opens in a new tab) component from the
@openmrs/esm-framework
package to render the link. Thetitle
property gets rendered as the link text. The link also gets some special styling if thename
property matches the last portion of the URL that distinguishes it as the active link. - We're using the
useLocation
hook from thereact-router-dom
package to get the current URL. We then extract the last segment of the current pathname and decode it. This segment is stored in theurlSegment
variable. We then use theurlSegment
variable to determine whether the link is active or not. - We're using the
createLeftPanelLink
higher-order function to create a component that renders the link. This function takes aname
and atitle
property. Thename
property is the unique path that the URL segment gets matched against. If the name matches the last portion of the URL, then the matching link gets some special styling to indicate that it is the active link. Thetitle
property gets rendered as the link text. - When the last segment of the URL is a UUID (which is the case when you click on a ward card on the landing page), we're setting the
urlSegment
variable tosummary
. This ensures that both the landing and detail screen s get theSummary
link highlighted as the active link. - Finally, we're exporting the
createLeftPanelLink
function so that it can be used to create theSummary
andWard allocation
links.
Step 4: Add the extensions to the routes.json file
Finally, we'll need to add the two extensions that we created in the previous step to the extensions
array of our routes.json
file as shown below:
"extensions": [
{
"component": "adminCardLink",
"name": "bed-management-admin-card-link",
"slot": "system-admin-page-card-link-slot"
},
{
"component": "summaryLeftPanelLink",
"name": "bed-management-left-panel-link",
"slot": "bed-management-left-panel-slot",
"order": 0
},
{
"component": "wardAllocationLeftPanelLink",
"name": "ward-allocation-left-panel-link",
"slot": "bed-management-left-panel-slot"
}
]
Step 5: Profit!
That's it! When you navigate to the bed-management
route, you should see the landing screen of the Bed Management app, which should look like the following:
Clicking on the General Men Ward
card navigates you to a detail page for a specific ward, which should look like the following:
Finally, clicking the Ward Allocation
link in the left panel leads you to the Ward Allocation page, which looks like the following: