-
Notifications
You must be signed in to change notification settings - Fork 1.1k
[ENG-749] feat: add search functionality for facilities and other in Dashboard and Facility Switcher #16566
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
base: develop
Are you sure you want to change the base?
[ENG-749] feat: add search functionality for facilities and other in Dashboard and Facility Switcher #16566
Changes from 4 commits
3bb4251
7d08316
6de2604
1a8879d
9bd46be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,10 @@ | ||
| import { ChevronRight, LogOut, SquarePen, User2Icon } from "lucide-react"; | ||
| import { | ||
| ChevronRight, | ||
| LogOut, | ||
| Search, | ||
| SquarePen, | ||
| User2Icon, | ||
| } from "lucide-react"; | ||
| import { Link } from "raviger"; | ||
| import { useState } from "react"; | ||
| import { useTranslation } from "react-i18next"; | ||
|
|
@@ -22,6 +28,8 @@ import useAuthUser, { useAuthContext } from "@/hooks/useAuthUser"; | |
| import useBreakpoints from "@/hooks/useBreakpoints"; | ||
|
|
||
| import { formatName } from "@/Utils/utils"; | ||
| import { EmptyState } from "@/components/ui/empty-state"; | ||
| import { Input } from "@/components/ui/input"; | ||
| import { FacilityBareMinimum } from "@/types/facility/facility"; | ||
| import { Organization, getOrgLabel } from "@/types/organization/organization"; | ||
|
|
||
|
|
@@ -37,6 +45,8 @@ type TabContentProps = { | |
| description: string; | ||
| renderChild: (item: FacilityBareMinimum | Organization) => React.ReactNode; | ||
| isLoading?: boolean; | ||
| searchComponent?: React.ReactNode; | ||
| emptyStateTitle?: string; | ||
| }; | ||
|
|
||
| export default function UserDashboard() { | ||
|
|
@@ -79,6 +89,11 @@ export default function UserDashboard() { | |
| availableTabs.length > 0 ? availableTabs[0] : null, | ||
| ); | ||
|
|
||
| const [facilitySearch, setFacilitySearch] = useState(""); | ||
| const filteredFacilities = facilities.filter((facility) => | ||
| facility.name.toLowerCase().includes(facilitySearch.toLowerCase()), | ||
| ); | ||
|
|
||
|
abhimanyurajeesh marked this conversation as resolved.
Outdated
|
||
| const isMobile = useBreakpoints({ default: true, sm: false }); | ||
|
|
||
| return ( | ||
|
|
@@ -184,7 +199,7 @@ export default function UserDashboard() { | |
| {availableTabs.length > 0 && ( | ||
| <div className="w-full"> | ||
| <div | ||
| className="flex border-b border-gray-200" | ||
| className="flex border-b border-gray-200 overflow-auto" | ||
| role="tablist" | ||
| aria-label="Dashboard Sections" | ||
| > | ||
|
|
@@ -212,8 +227,23 @@ export default function UserDashboard() { | |
| {activeTab === DashboardTabs.TAB_FACILITIES && ( | ||
| <TabContent | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can just inline a new component here for just facilities with search, input, card etc, instead of changing the shape of TabContent. If we ever add search to other tabs, we can adjust it then. |
||
| tabId="facilities-panel" | ||
| tabItems={facilities} | ||
| tabItems={filteredFacilities} | ||
| description={t("dashboard_tab_facilities")} | ||
| searchComponent={ | ||
| facilities.length > 1 ? ( | ||
| <div className="relative"> | ||
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" /> | ||
| <Input | ||
| placeholder={t("search_button")} | ||
| aria-label={t("search_button")} | ||
| value={facilitySearch} | ||
| onChange={(e) => setFacilitySearch(e.target.value)} | ||
| className="pl-9" | ||
| /> | ||
| </div> | ||
| ) : undefined | ||
| } | ||
| emptyStateTitle={t("no_facilities_found")} | ||
| renderChild={(facility) => { | ||
| return ( | ||
| <Link | ||
|
|
@@ -332,7 +362,10 @@ const TabContent = ({ | |
| description, | ||
| renderChild, | ||
| isLoading, | ||
| searchComponent, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lift these up/remove props |
||
| emptyStateTitle, | ||
| }: TabContentProps) => { | ||
| const { t } = useTranslation(); | ||
| return ( | ||
| <section | ||
| className="space-y-3 md:space-y-4" | ||
|
|
@@ -341,6 +374,7 @@ const TabContent = ({ | |
| aria-labelledby={tabId} | ||
|
Comment on lines
351
to
354
|
||
| > | ||
|
abhimanyurajeesh marked this conversation as resolved.
|
||
| <p className="text-sm text-gray-800 font-normal px-1">{description}</p> | ||
| {searchComponent} | ||
|
|
||
| {isLoading ? ( | ||
| <div className="grid gap-3 md:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> | ||
|
|
@@ -356,6 +390,12 @@ const TabContent = ({ | |
| </Card> | ||
| ))} | ||
| </div> | ||
| ) : tabItems.length === 0 ? ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specific to facilities, but technically won't ever show up for other tabs. Hmm, I think we can lift this up. |
||
| <EmptyState | ||
| icon={<Search className="size-5 text-primary" />} | ||
| title={emptyStateTitle || t("no_results_found")} | ||
| className="border-solid" | ||
| /> | ||
|
abhimanyurajeesh marked this conversation as resolved.
Outdated
|
||
| ) : ( | ||
| <div className="grid gap-3 md:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> | ||
| {tabItems.map((item: FacilityBareMinimum | Organization) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { expect, test } from "@playwright/test"; | ||
|
|
||
| test.use({ storageState: "tests/.auth/user.json" }); | ||
|
|
||
| test.describe("Dashboard - Facility Search", () => { | ||
| test("should filter facilities in the Dashboard", async ({ page }) => { | ||
| await page.goto(`/`); | ||
| await expect(page.getByRole("tab", { name: "Facilities" })).toBeVisible(); | ||
| const searchInput = page.getByPlaceholder(/Search Facilities/i); | ||
|
|
||
| const isSearchVisible = await searchInput.isVisible().catch(() => false); | ||
| if (!isSearchVisible) { | ||
| test.skip(); | ||
| return; | ||
| } | ||
|
|
||
|
|
||
| await test.step("Search for a facility FACILITY WITH PATIENTS", async () => { | ||
| const facilityName = "FACILITY WITH PATIENTS"; | ||
|
|
||
| await searchInput.fill(facilityName); | ||
|
|
||
| // Verify matching facility is still visible | ||
| await expect( | ||
| page.getByRole("link").filter({ hasText: facilityName }), | ||
| ).toBeVisible(); | ||
|
|
||
| }); | ||
|
|
||
| await test.step("Show empty state for non-matching search", async () => { | ||
| await searchInput.fill("zzz_nonexistent_facility_xyz"); | ||
| await expect(page.getByText(/no facilities found/i)).toBeVisible(); | ||
|
|
||
| // Ensure more than one facility link is shown after clearing search | ||
| await searchInput.clear(); | ||
| const facilities = await page.getByRole("link").all(); | ||
| expect(facilities.length).toBeGreaterThan(2); | ||
|
abhimanyurajeesh marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
abhimanyurajeesh marked this conversation as resolved.
Outdated
|
||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { expect, test } from "@playwright/test"; | ||
| import { getFacilityId } from "tests/support/facilityId"; | ||
|
|
||
| test.use({ storageState: "tests/.auth/user.json" }); | ||
|
|
||
| test.describe("Sidebar Facility Switcher - Search", () => { | ||
| test("should filter facilities in the switcher dropdown", async ({ | ||
| page, | ||
| }) => { | ||
| await page.goto(`/facility/${getFacilityId()}/overview`); | ||
|
|
||
| //switcher | ||
| await page.getByRole("button", { name: "Select Facility" }).click(); | ||
| await expect(page.getByText(/view dashboard/i)).toBeVisible(); | ||
| await expect(page.getByText(/Facilities/i)).toBeVisible(); | ||
|
|
||
| const searchInput = page.getByPlaceholder(/search/i); | ||
|
|
||
|
abhimanyurajeesh marked this conversation as resolved.
|
||
| const isSearchVisible = await searchInput.isVisible().catch(() => false); | ||
| if (!isSearchVisible) { | ||
| test.skip(); | ||
| return; | ||
| } | ||
|
abhimanyurajeesh marked this conversation as resolved.
Comment on lines
+19
to
+23
|
||
|
|
||
| await test.step("Search for a facility FACILITY WITH PATIENTS", async () => { | ||
| const facilityName = "FACILITY WITH PATIENTS"; | ||
|
|
||
| await searchInput.fill(facilityName); | ||
|
|
||
| // Verify matching facility is still visible | ||
| await expect( | ||
| page.getByRole("menuitem").filter({ hasText: facilityName }), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| await test.step("Show empty state for non-matching search", async () => { | ||
| await searchInput.fill("zzz_nonexistent_facility_xyz"); | ||
| await expect(page.getByText(/no facilities found/i)).toBeVisible(); | ||
|
|
||
| // Ensure more than one facility menuitem is shown after clearing search | ||
| await searchInput.clear(); | ||
| const facilities = await page.getByRole("menuitem").all(); | ||
| expect(facilities.length).toBeGreaterThan(1); | ||
|
Comment on lines
+40
to
+43
|
||
| }); | ||
|
abhimanyurajeesh marked this conversation as resolved.
|
||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.