-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Make Virtuoso a HOC at the root of the Search component
- Loading branch information
1 parent
aef4fde
commit 76efb27
Showing
6 changed files
with
247 additions
and
245 deletions.
There are no files selected for viewing
59 changes: 0 additions & 59 deletions
59
editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.test.tsx
This file was deleted.
Oops, something went wrong.
67 changes: 0 additions & 67 deletions
67
editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx
This file was deleted.
Oops, something went wrong.
64 changes: 64 additions & 0 deletions
64
editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchHeader.test.tsx
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,64 @@ | ||
import { waitFor } from "@testing-library/react"; | ||
import { useStore } from "pages/FlowEditor/lib/store"; | ||
import React from "react"; | ||
import { setup } from "testUtils"; | ||
import { vi } from "vitest"; | ||
|
||
import Search from "."; | ||
import { flow } from "./mocks/simple"; | ||
import { VirtuosoWrapper } from "./testUtils"; | ||
|
||
vi.mock("react-navi", () => ({ | ||
useNavigation: () => ({ | ||
navigate: vi.fn(), | ||
}), | ||
})); | ||
|
||
beforeAll(() => useStore.setState({ flow })); | ||
|
||
it("Displays a warning if no results are returned", async () => { | ||
const { getByLabelText, getByText, getByRole, user } = setup( | ||
<VirtuosoWrapper> | ||
<Search /> | ||
</VirtuosoWrapper>, | ||
); | ||
|
||
const searchInput = getByLabelText("Search this flow and internal portals"); | ||
user.type(searchInput, "Timbuktu"); | ||
|
||
await waitFor(() => | ||
expect(getByText("No matches found")).toBeInTheDocument(), | ||
); | ||
expect(getByRole("list")).toBeEmptyDOMElement(); | ||
}); | ||
|
||
it("Displays the count for a single result", async () => { | ||
const { getByLabelText, getByText, getAllByRole, getByRole, user } = setup( | ||
<VirtuosoWrapper> | ||
<Search /> | ||
</VirtuosoWrapper>, | ||
); | ||
|
||
const searchInput = getByLabelText("Search this flow and internal portals"); | ||
user.type(searchInput, "Spain"); | ||
|
||
await waitFor(() => expect(getByText("1 result:")).toBeInTheDocument()); | ||
expect(getByRole("list")).not.toBeEmptyDOMElement(); | ||
expect(getAllByRole("listitem")).toHaveLength(1); | ||
}); | ||
|
||
it("Displays the count for multiple results", async () => { | ||
const { getByText, getByRole, getAllByRole, getByLabelText, user } = setup( | ||
<VirtuosoWrapper> | ||
<Search /> | ||
</VirtuosoWrapper>, | ||
); | ||
|
||
const searchInput = getByLabelText("Search this flow and internal portals"); | ||
// Matches "India" and "Indonesia" | ||
user.type(searchInput, "Ind"); | ||
|
||
await waitFor(() => expect(getByText("2 results:")).toBeInTheDocument()); | ||
expect(getByRole("list")).not.toBeEmptyDOMElement(); | ||
expect(getAllByRole("listitem")).toHaveLength(2); | ||
}); |
130 changes: 130 additions & 0 deletions
130
editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchHeader.tsx
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,130 @@ | ||
import Box from "@mui/material/Box"; | ||
import CircularProgress from "@mui/material/CircularProgress"; | ||
import Typography from "@mui/material/Typography"; | ||
import { useFormik } from "formik"; | ||
import { useSearch } from "hooks/useSearch"; | ||
import { debounce } from "lodash"; | ||
import { useStore } from "pages/FlowEditor/lib/store"; | ||
import React, { useEffect, useMemo, useState } from "react"; | ||
import { Components } from "react-virtuoso"; | ||
import ChecklistItem from "ui/shared/ChecklistItem"; | ||
import Input from "ui/shared/Input"; | ||
|
||
import { Context, Data } from "."; | ||
import { DATA_FACETS } from "./facets"; | ||
|
||
const DEBOUNCE_MS = 500; | ||
|
||
interface SearchNodes { | ||
pattern: string; | ||
facets: typeof DATA_FACETS; | ||
} | ||
|
||
/** | ||
* SearchHeader contains the main logic of the search sidebar | ||
* It is nested within the Virtuoso list as a header to allow scrolling to work across the entire sidebar | ||
*/ | ||
export const SearchHeader: Components<Data, Context>["Header"] = ({ | ||
context, | ||
}) => { | ||
// Get ordered flow of indexed nodes from store | ||
const [orderedFlow, setOrderedFlow] = useStore((state) => [ | ||
state.orderedFlow, | ||
state.setOrderedFlow, | ||
]); | ||
|
||
useEffect(() => { | ||
if (!orderedFlow) setOrderedFlow(); | ||
}, [orderedFlow, setOrderedFlow]); | ||
|
||
// Set up search input form | ||
const formik = useFormik<SearchNodes>({ | ||
initialValues: { pattern: "", facets: DATA_FACETS }, | ||
onSubmit: ({ pattern }) => { | ||
debouncedSearch(pattern); | ||
}, | ||
}); | ||
|
||
// Set up spinner UI in search bar | ||
const [isSearching, setIsSearching] = useState(false); | ||
const [lastSearchedTerm, setLastSearchedTerm] = useState(""); | ||
|
||
useEffect(() => { | ||
if (formik.values.pattern !== lastSearchedTerm) { | ||
setIsSearching(true); | ||
} | ||
}, [formik.values.pattern, lastSearchedTerm]); | ||
|
||
// Call custom hook to control searching | ||
const { results, search } = useSearch({ | ||
list: orderedFlow || [], | ||
keys: formik.values.facets, | ||
}); | ||
|
||
const debouncedSearch = useMemo( | ||
() => | ||
debounce((pattern: string) => { | ||
console.debug("Search term: ", pattern); | ||
search(pattern); | ||
setLastSearchedTerm(pattern); | ||
setIsSearching(false); | ||
}, DEBOUNCE_MS), | ||
[search], | ||
); | ||
|
||
// Update results in parent component (Virtuoso list) | ||
useEffect(() => { | ||
context?.setResults(results); | ||
}, [context, results]); | ||
|
||
return ( | ||
<Box mx={3} component="form" onSubmit={formik.handleSubmit}> | ||
<Typography | ||
component={"label"} | ||
htmlFor="pattern" | ||
variant="h3" | ||
mb={1} | ||
display={"block"} | ||
> | ||
Search this flow and internal portals | ||
</Typography> | ||
<Box sx={{ display: "flex", position: "relative", alignItems: "center" }}> | ||
<Input | ||
id="pattern" | ||
name="pattern" | ||
value={formik.values.pattern} | ||
onChange={(e) => { | ||
formik.setFieldValue("pattern", e.target.value); | ||
formik.handleSubmit(); | ||
}} | ||
inputProps={{ spellCheck: false }} | ||
/> | ||
{isSearching && ( | ||
<CircularProgress | ||
size={25} | ||
sx={(theme) => ({ | ||
position: "absolute", | ||
right: theme.spacing(1.5), | ||
zIndex: 1, | ||
})} | ||
/> | ||
)} | ||
</Box> | ||
<ChecklistItem | ||
label="Search only data fields" | ||
id={"search-data-field-facet"} | ||
checked | ||
inputProps={{ disabled: true }} | ||
onChange={() => {}} | ||
variant="compact" | ||
/> | ||
{formik.values.pattern && ( | ||
<Typography variant="h3" mt={2} mb={1}> | ||
{context?.resultCount === 0 && "No matches found"} | ||
{context?.resultCount === 1 && "1 result:"} | ||
{context!.resultCount > 1 && `${context?.resultCount} results:`} | ||
</Typography> | ||
)} | ||
</Box> | ||
); | ||
}; |
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
Oops, something went wrong.