Skip to content

Commit

Permalink
feat(wip): Initial proof of concept with Fuse.js
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Jul 17, 2024
1 parent 788e811 commit 660c748
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 58 deletions.
1 change: 1 addition & 0 deletions editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"dompurify": "^3.0.6",
"dotenv": "^16.4.5",
"formik": "^2.4.5",
"fuse.js": "^7.0.0",
"graphql": "^16.8.1",
"graphql-tag": "^2.12.6",
"immer": "^9.0.21",
Expand Down
8 changes: 8 additions & 0 deletions editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions editor.planx.uk/src/hooks/useSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Fuse, { IFuseOptions } from "fuse.js";
import { useEffect, useMemo, useState } from "react";

const DEFAULT_OPTIONS: Required<SearchOptions> = {
limit: 20,
};

interface SearchOptions {
limit?: number;
}

interface UseSearchProps<T extends Record<string, unknown>> {
list: T[];
keys: string[];
options?: SearchOptions;
}

export const useSearch = <T extends Record<string, unknown>>({
list,
keys,
options,
}: UseSearchProps<T>) => {
const [pattern, setPattern] = useState("");
const [results, setResults] = useState<T[]>([]);

const fuseOptions: IFuseOptions<T> = useMemo(
() => ({
threshold: 0.3,
useExtendedSearch: true,
keys,
}),
[keys],
);

const fuse = useMemo(
() => new Fuse<T>(list, fuseOptions),
[list, fuseOptions],
);

useEffect(() => {
const fuseResults = fuse.search(pattern, {
limit: options?.limit || DEFAULT_OPTIONS.limit,
});
setResults(fuseResults.map((result) => result.item));
}, [pattern]);

return { results, search: setPattern };
};
103 changes: 45 additions & 58 deletions editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,20 @@ import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import { ComponentType } from "@opensystemslab/planx-core/types";
import { ICONS } from "@planx/components/ui";
import { debounce } from "lodash";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { ChangeEvent, useCallback, useState } from "react";
import useSWR from "swr";
import { useSearch } from "hooks/useSearch";
import { Store, useStore } from "pages/FlowEditor/lib/store";
import React, { ChangeEvent, useMemo } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import InputLabel from "ui/editor/InputLabel";
import ChecklistItem from "ui/shared/ChecklistItem";
import Input from "ui/shared/Input";

const mockData: SearchResult[] = [
{
nodeId: "abc123",
nodeType: ComponentType.Question,
nodeTitle: "Is the property in Lambeth?",
text: "Lambeth example biodiversity text",
path: ["_root", "xyz123", "abc123"],
},
{
nodeId: "abc456",
nodeType: ComponentType.Notice,
nodeTitle: "It looks like the property is not in Lambeth",
text: "Lambeth example biodiversity text",
path: ["_root", "xyz123", "abc123"],
},
{
nodeId: "abc789",
nodeType: ComponentType.Question,
nodeTitle: "What are you applying about?",
text: "Lambeth example biodiversity text",
path: ["_root", "xyz123", "abc123"],
},
];

export interface SearchResult {
id?: Store.nodeId;
type?: ComponentType;
data?: any;
edges?: Store.nodeId[];
nodeId: string;
nodeType: ComponentType;
nodeTitle?: string;
text: string;
path: string[];
}

const SearchResults: React.FC<{ results: SearchResult[] }> = ({ results }) => {
Expand All @@ -56,12 +31,9 @@ const SearchResults: React.FC<{ results: SearchResult[] }> = ({ results }) => {
);
};

const SearchResultCard: React.FC<SearchResult> = ({
nodeTitle,
text,
nodeType,
}) => {
const Icon = ICONS[nodeType];
// TODO: This likely needs to be related to facets?
const SearchResultCard: React.FC<SearchResult> = ({ data, type }) => {
const Icon = ICONS[type!];

return (
<Box
Expand All @@ -78,37 +50,53 @@ const SearchResultCard: React.FC<SearchResult> = ({
fontWeight={FONT_WEIGHT_SEMI_BOLD}
>
Question
{nodeTitle && ` - ${nodeTitle}`}
{data.text && ` - ${data.text}`}
</Typography>
</Box>
<Typography variant="body2">{text}</Typography>
<Typography
variant="body2"
sx={{
backgroundColor: "#f0f0f0",
borderColor: "#d3d3d3",
fontFamily: `"Source Code Pro", monospace;`,
}}
>
{data.fn || data.val}
</Typography>
</Box>
);
};

const Search: React.FC = () => {
const [flowId] = useStore((state) => [state.id]);
const [query, setQuery] = useState("");
const [debouncedQuery, setDebouncedQuery] = useState(query);

const debounceSearch = useCallback(
debounce((input) => setDebouncedQuery(input), 500),
[],
const nodes = useStore((state) => state.flow);
// TODO: add to store?
// TODO: think about parentIds
const nodeList = useMemo(
() =>
Object.entries(nodes).map(([nodeId, nodeData]) => ({
nodeId,
...nodeData,
})),
[nodes],
);

/** Map of search facets to associated node keys */
const facets = {
data: ["data.fn", "data.val"],
};

const { results, search } = useSearch({
list: nodeList,
keys: facets.data,
});

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const input = e.target.value;
setQuery(input);
debounceSearch(input);
console.log({ input });
search(input);
console.log(results);
};

const fetcher = (url: string) => fetch(url).then((r) => r.json());
const endpoint = `${process.env.REACT_APP_API_URL}/flows/${flowId}/search`;
const { data, error } = useSWR<SearchResult[]>(
debouncedQuery ? `${endpoint}?find=${debouncedQuery}` : null,
fetcher,
);

return (
<Container component={Box} p={3}>
<InputLabel label="Type to search" htmlFor="search">
Expand All @@ -124,8 +112,7 @@ const Search: React.FC = () => {
onChange={() => {}}
/>
<Box pt={3}>
{error && "Something went wrong"}
{mockData ? <SearchResults results={mockData} /> : "Loading..."}
{results ? <SearchResults results={results} /> : "Loading..."}
</Box>
</Container>
);
Expand Down

0 comments on commit 660c748

Please sign in to comment.