Skip to content

Commit

Permalink
refactor: Make Virtuoso a HOC at the root of the Search component
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Sep 30, 2024
1 parent aef4fde commit 76efb27
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 245 deletions.

This file was deleted.

This file was deleted.

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);
});
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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ test("entering a search term displays a series of cards", async () => {
</VirtuosoWrapper>,
);

expect(queryByRole("list")).not.toBeInTheDocument();
expect(queryByRole("list")).toBeEmptyDOMElement();

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");
Expand Down
Loading

0 comments on commit 76efb27

Please sign in to comment.