diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx deleted file mode 100644 index 7b25315d9a..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import Box from "@mui/material/Box"; -import Container from "@mui/material/Container"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import { styled, Theme } from "@mui/material/styles"; -import Typography from "@mui/material/Typography"; -import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; -import { ICONS } from "@planx/components/ui"; -import { useFormik } from "formik"; -import type { SearchResult, SearchResults } from "hooks/useSearch"; -import { useSearch } from "hooks/useSearch"; -import { capitalize, get } from "lodash"; -import { SLUGS } from "pages/FlowEditor/data/types"; -import { useStore } from "pages/FlowEditor/lib/store"; -import React, { useEffect } from "react"; -import { useNavigation } from "react-navi"; -import { FONT_WEIGHT_BOLD, FONT_WEIGHT_SEMI_BOLD } from "theme"; -import ChecklistItem from "ui/shared/ChecklistItem"; -import Input from "ui/shared/Input"; - -const SearchResultRoot = styled(List)(({ theme }) => ({ - width: "100%", - gap: theme.spacing(2), - display: "flex", - flexDirection: "column", -})); - -const SearchResultCardRoot = styled(ListItemButton)(({ theme }) => ({ - padding: theme.spacing(1), - border: `1px solid ${theme.palette.common.black}`, - display: "block", -})); - -const PortalList = styled(List)(({ theme }) => ({ - color: theme.palette.text.primary, - padding: theme.spacing(0.5, 0), - backgroundColor: theme.palette.background.paper, - border: `1px solid ${theme.palette.border.light}`, -})); - -const SearchResults: React.FC<{ results: SearchResults }> = ({ - results, -}) => { - return ( - <> - - {!results.length && "No matches found"} - {results.length === 1 && "1 result:"} - {results.length > 1 && `${results.length} results:`} - - - - {results.map((result) => ( - - - - ))} - - - ); -}; - -interface HeadlineProps { - text: string; - matchIndices: [number, number][]; - variant: "data"; -} - -const Headline: React.FC = ({ text, matchIndices, variant }) => { - const isHighlighted = (index: number) => - matchIndices.some(([start, end]) => index >= start && index <= end); - - return ( - <> - {text.split("").map((char, index) => ( - ({ - fontWeight: isHighlighted(index) ? FONT_WEIGHT_BOLD : "regular", - fontSize: theme.typography.body2.fontSize, - })} - > - {char} - - ))} - - ); -}; - -const SearchResultCard: React.FC<{ result: SearchResult }> = ({ - result, -}) => { - const getDisplayDetailsForResult = ({ - item, - key, - }: SearchResult) => { - const componentType = capitalize( - SLUGS[result.item.type].replaceAll("-", " "), - ); - let title = (item.data?.title as string) || (item.data?.text as string); - let Icon = ICONS[item.type]; - // TODO: Generate display key from key - let displayKey = "Data"; - const headline = get(item, key).toString() || ""; - - // For Answer nodes, update display values to match the parent question - if (item.type === ComponentType.Answer) { - const parentNode = useStore.getState().flow[item.parentId]; - Icon = ICONS[ComponentType.Question]; - title = parentNode!.data.text!; - displayKey = "Option (data)"; - } - - return { - Icon, - componentType, - title, - key: displayKey, - headline, - }; - }; - - const { Icon, componentType, title, key, headline } = - getDisplayDetailsForResult(result); - - // TODO - display portal wrapper - - const handleClick = () => { - console.log("todo!"); - console.log({ nodeId: result.item.id }); - // get path for node - // generate url from path - // navigate to url - }; - - return ( - - - {Icon && } - - {componentType} - - {title && ( - - {` • ${title}`} - - )} - - - {key} - - - - - ); -}; - -const ExternalPortalList: React.FC = () => { - const externalPortals = useStore((state) => state.externalPortals); - const hasExternalPortals = Object.keys(externalPortals).length; - const { navigate } = useNavigation(); - - if (!hasExternalPortals) return null; - - return ( - - - Your service also contains the following external portals, which have - not been searched: - - - {Object.values(externalPortals).map(({ name, href }) => ( - - - - {href.replaceAll("/", " / ")} - - - - ))} - - - ); -}; - -interface SearchNodes { - input: string; - facets: ["data.fn", "data.val"]; -} - -const Search: React.FC = () => { - const [orderedFlow, setOrderedFlow] = useStore((state) => [ - state.orderedFlow, - state.setOrderedFlow, - ]); - - useEffect(() => { - if (!orderedFlow) setOrderedFlow(); - }, [setOrderedFlow]); - - const formik = useFormik({ - initialValues: { input: "", facets: ["data.fn", "data.val"] }, - onSubmit: ({ input }) => { - search(input); - }, - }); - - const { results, search } = useSearch({ - list: orderedFlow || [], - keys: formik.values.facets, - }); - - return ( - -
- - Search this flow and internal portals - - { - formik.setFieldValue("input", e.target.value); - formik.handleSubmit(); - }} - inputProps={{ spellCheck: false }} - /> - {}} - /> - - {formik.values.input && ( - <> - - - - )} - - -
- ); -}; - -export default Search; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/ExternalPortalList.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/ExternalPortalList.tsx new file mode 100644 index 0000000000..c3e578fa68 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/ExternalPortalList.tsx @@ -0,0 +1,42 @@ +import Box from "@mui/material/Box"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; + +export const Root = styled(List)(({ theme }) => ({ + color: theme.palette.text.primary, + padding: theme.spacing(0.5, 0), + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.border.light}`, +})); + +export const ExternalPortalList: React.FC = () => { + const externalPortals = useStore((state) => state.externalPortals); + const hasExternalPortals = Object.keys(externalPortals).length; + + if (!hasExternalPortals) return null; + + return ( + + + Your service also contains the following external portals, which have + not been searched: + + + {Object.values(externalPortals).map(({ name, href }) => ( + + + + {href.replaceAll("/", " / ")} + + + + ))} + + + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.tsx new file mode 100644 index 0000000000..5681e19324 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.tsx @@ -0,0 +1,32 @@ +import Typography from "@mui/material/Typography"; +import React from "react"; +import { FONT_WEIGHT_BOLD } from "theme"; + +interface Props { + text: string; + matchIndices: [number, number][]; + variant: "data"; +} + +export const Headline: React.FC = ({ text, matchIndices }) => { + const isHighlighted = (index: number) => + matchIndices.some(([start, end]) => index >= start && index <= end); + + return ( + <> + {text.split("").map((char, index) => ( + ({ + fontWeight: isHighlighted(index) ? FONT_WEIGHT_BOLD : "regular", + fontSize: theme.typography.body2.fontSize, + })} + > + {char} + + ))} + + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx new file mode 100644 index 0000000000..fcbb5d8daf --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx @@ -0,0 +1,38 @@ +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import { IndexedNode } from "@opensystemslab/planx-core/types"; +import type { SearchResults } from "hooks/useSearch"; +import React from "react"; + +import { SearchResultCard } from "./SearchResultCard"; + +export const Root = styled(List)(({ theme }) => ({ + width: "100%", + gap: theme.spacing(2), + display: "flex", + flexDirection: "column", +})); + +export const NodeSearchResults: React.FC<{ + results: SearchResults; +}> = ({ results }) => { + return ( + <> + + {!results.length && "No matches found"} + {results.length === 1 && "1 result:"} + {results.length > 1 && `${results.length} results:`} + + + + {results.map((result) => ( + + + + ))} + + + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx new file mode 100644 index 0000000000..30f5621d2a --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx @@ -0,0 +1,112 @@ +import Box from "@mui/material/Box"; +import ListItemButton from "@mui/material/ListItemButton"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { ICONS } from "@planx/components/ui"; +import type { SearchResult } from "hooks/useSearch"; +import { capitalize, get } from "lodash"; +import { SLUGS } from "pages/FlowEditor/data/types"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { FONT_WEIGHT_SEMI_BOLD } from "theme"; + +import { Headline } from "./Headline"; + +export const Root = styled(ListItemButton)(({ theme }) => ({ + padding: theme.spacing(1), + border: `1px solid ${theme.palette.common.black}`, + display: "block", +})); + +export const SearchResultCard: React.FC<{ + result: SearchResult; +}> = ({ result }) => { + const getDisplayDetailsForResult = ({ + item, + key, + }: SearchResult) => { + const componentType = capitalize( + SLUGS[result.item.type].replaceAll("-", " "), + ); + let title = (item.data?.title as string) || (item.data?.text as string); + let Icon = ICONS[item.type]; // TODO: Generate display key from key + + let displayKey = "Data"; + const headline = get(item, key).toString() || ""; + + // For Answer nodes, update display values to match the parent question + if (item.type === ComponentType.Answer) { + const parentNode = useStore.getState().flow[item.parentId]; + Icon = ICONS[ComponentType.Question]; + title = parentNode!.data.text!; + displayKey = "Option (data)"; + } + + return { + Icon, + componentType, + title, + key: displayKey, + headline, + }; + }; + + const { Icon, componentType, title, key, headline } = + getDisplayDetailsForResult(result); // TODO - display portal wrapper + + const handleClick = () => { + console.log("todo!"); + console.log({ nodeId: result.item.id }); + // get path for node + // generate url from path + // navigate to url + }; + + return ( + + + {Icon && ( + + )} + + {componentType} + + {title && ( + + {` • ${title}`} + + )} + + + {key} - + + + + ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx new file mode 100644 index 0000000000..a7f9a825f5 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx @@ -0,0 +1,82 @@ +import Box from "@mui/material/Box"; +import Container from "@mui/material/Container"; +import Typography from "@mui/material/Typography"; +import { useFormik } from "formik"; +import { useSearch } from "hooks/useSearch"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React, { useEffect } from "react"; +import ChecklistItem from "ui/shared/ChecklistItem"; +import Input from "ui/shared/Input"; + +import { ExternalPortalList } from "./ExternalPortalList"; +import { NodeSearchResults } from "./NodeSearchResults"; + +interface SearchNodes { + input: string; + facets: ["data.fn", "data.val"]; +} + +const Search: React.FC = () => { + const [orderedFlow, setOrderedFlow] = useStore((state) => [ + state.orderedFlow, + state.setOrderedFlow, + ]); + + useEffect(() => { + if (!orderedFlow) setOrderedFlow(); + }, [orderedFlow, setOrderedFlow]); + + const formik = useFormik({ + initialValues: { input: "", facets: ["data.fn", "data.val"] }, + onSubmit: ({ input }) => { + search(input); + }, + }); + + const { results, search } = useSearch({ + list: orderedFlow || [], + keys: formik.values.facets, + }); + + return ( + +
+ + Search this flow and internal portals + + { + formik.setFieldValue("input", e.target.value); + formik.handleSubmit(); + }} + inputProps={{ spellCheck: false }} + /> + {}} + /> + + {formik.values.input && ( + <> + + + + )} + + +
+ ); +}; + +export default Search;