Skip to content

Commit

Permalink
feat: Style updates
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Jul 19, 2024
1 parent 00e0022 commit e64dec4
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 65 deletions.
21 changes: 19 additions & 2 deletions editor.planx.uk/src/hooks/useSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@ interface UseSearchProps<T extends object> {
keys: string[];
}

export interface SearchResult<T extends object> {
item: T;
key: string;
matchIndices?: [number, number][],
}

export type SearchResults<T extends object> = SearchResult<T>[]

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

const fuseOptions: IFuseOptions<T> = useMemo(
() => ({
useExtendedSearch: true,
includeMatches: true,
minMatchCharLength: 3,
keys,
}),
[keys],
Expand All @@ -28,7 +38,14 @@ export const useSearch = <T extends object>({

useEffect(() => {
const fuseResults = fuse.search(pattern);
setResults(fuseResults.map((result) => result.item));
setResults(
fuseResults.map((result) => ({
item: result.item,
key: result.matches?.[0].key || "",
// We only display the first match
matchIndices: result.matches?.[0].indices as [number, number][] || undefined,
}))
);
}, [pattern]);

return { results, search: setPattern };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Hanger from "./Hanger";
import Question from "./Question";

const ExternalPortal: React.FC<any> = (props) => {
const copyNode = useStore((state) => state.copyNode);
const [copyNode, addExternalPortal] = useStore((state) => [state.copyNode, state.addExternalPortal]);
const [href, setHref] = useState("Loading...");

const { data, loading } = useQuery(
Expand All @@ -23,6 +23,7 @@ const ExternalPortal: React.FC<any> = (props) => {
flows_by_pk(id: $id) {
id
slug
name
team {
slug
}
Expand All @@ -31,8 +32,11 @@ const ExternalPortal: React.FC<any> = (props) => {
`,
{
variables: { id: props.data.flowId },
onCompleted: (data) =>
setHref([data.flows_by_pk.team.slug, data.flows_by_pk.slug].join("/")),
onCompleted: (data) => {
const href = [data.flows_by_pk.team.slug, data.flows_by_pk.slug].join("/")
setHref(href);
addExternalPortal({ id: props.data.flowId, name: data.flows_by_pk.name, href })
},
},
);

Expand Down
201 changes: 158 additions & 43 deletions editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,106 @@
import { styled } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import { IndexedNode, OrderedFlow } from "@opensystemslab/planx-core/types";
import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types";
import { ICONS } from "@planx/components/ui";
import { useSearch } from "hooks/useSearch";
import type { SearchResults, 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, { ChangeEvent, useEffect } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import InputLabel from "ui/editor/InputLabel";
import { FONT_WEIGHT_BOLD, FONT_WEIGHT_SEMI_BOLD } from "theme";
import ChecklistItem from "ui/shared/ChecklistItem";
import Input from "ui/shared/Input";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";

const SearchResults: React.FC<{ results: OrderedFlow }> = ({ results }) => {
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 SearchResults: React.FC<{ results: SearchResults<IndexedNode> }> = ({ results }) => {
return (
<Box
sx={{ width: "100%", gap: 2, display: "flex", flexDirection: "column" }}
>
{results.map((result) => (
<SearchResultCard key={result.id} {...result} />
<>
<Typography variant="h3" mb={1} >
{results.length} {results.length > 1 ? "results" : "result"}:
</Typography>
<SearchResultRoot>
{results.map((result) => (
<SearchResultCard key={result.item.id} result={result} />
))}
</SearchResultRoot>
</>
);
};

interface HeadlineProps {
text: string;
matchIndices: [number, number][],
variant: "data",
}

const Headline: React.FC<HeadlineProps> = ({ text, matchIndices, variant }) => {
const isHighlighted = (index: number) =>
matchIndices.some(([start, end]) => index >= start && index <= end);

return (
<>
{text.split("").map((char, index) => (
<Typography
fontWeight={isHighlighted(index) ? FONT_WEIGHT_BOLD : "regular"}
component="span"
p={0}
key={`headline-character-${index}`}
fontFamily={variant === "data" ? '"Source Code Pro", monospace' : "inherit"}
variant="body2"
>
{char}
</Typography>
))}
</Box>
</>
);
};

// TODO: This likely needs to be related to facets?
const SearchResultCard: React.FC<IndexedNode> = ({ data, type }) => {
const Icon = ICONS[type!];
const SearchResultCard: React.FC<{ result: SearchResult<IndexedNode> }> = ({ result }) => {
const getDisplayDetailsForResult = ({ item, key }: SearchResult<IndexedNode>) => {
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 = "Answer (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!")
Expand All @@ -35,38 +110,67 @@ const SearchResultCard: React.FC<IndexedNode> = ({ data, type }) => {
};

return (
<Box
sx={(theme) => ({
pb: 2,
borderBottom: `1px solid ${theme.palette.border.main}`,
})}
onClick={handleClick}
>
<SearchResultCardRoot onClick={handleClick}>
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
{Icon && <Icon sx={{ mr: 1 }} />}
<Typography
variant="body2"
fontSize={14}
fontWeight={FONT_WEIGHT_SEMI_BOLD}
>
Question
{data?.text && ` - ${data.text}`}
{componentType}
</Typography>
{title &&
<Typography
variant="body2"
fontSize={14}
ml={0.5}
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{` • ${title}`}

</Typography>
}
</Box>
<Typography
variant="body2"
sx={{
backgroundColor: "#f0f0f0",
borderColor: "#d3d3d3",
fontFamily: `"Source Code Pro", monospace;`,
}}
>
{(data?.fn as string) || (data?.val as string)}
<Typography variant="body2" display="inline-block" mr={0.5}>
{key} -
</Typography>
</Box>
<Headline text={headline} matchIndices={result.matchIndices!} variant="data" />
</SearchResultCardRoot>
);
};

const ExternalPortalList: React.FC = () => {
const externalPortals = useStore(state => state.externalPortals);
const hasExternalPortals = Object.keys(externalPortals).length;

if (!hasExternalPortals) return null;

return (
<Box pt={4}>
<Typography variant="body1" mb={2}>
Your service also contains the following external portals, which have not been searched:
</Typography>
{Object.values(externalPortals).map(({ name, href }) => (
<Box sx={(theme) => ({ backgroundColor: "black", color: theme.palette.common.white, mb: 2, p: 1 })} key={`external-portal-card-${name}`}>
<Typography variant="body2" fontWeight={FONT_WEIGHT_SEMI_BOLD} display="inline-block" mr={0.5}>
External portal •
</Typography>
<Typography variant="body2" component="a" href={"../" + href} sx={(theme) => ({
color: theme.palette.common.white,
textDecoration: "none",
borderBottom: "1px solid rgba(255, 255, 255, 0.75)",
})}>
{href}
</Typography>
</Box>
))}
</Box>
)
}

const Search: React.FC = () => {
const [orderedFlow, setOrderedFlow] = useStore((state) => [
state.orderedFlow,
Expand All @@ -89,27 +193,38 @@ const Search: React.FC = () => {

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

return (
<Container component={Box} p={3}>
<InputLabel label="Type to search" htmlFor="search">
<Input name="search" onChange={handleChange} id="boundaryUrl" />
</InputLabel>
<Typography
component={"label"}
htmlFor="search"
variant="h3" mb={1}
display={"block"}
>
Search this flow and internal portals
</Typography>
<Input
name="search"
onChange={handleChange}
inputProps={{ spellCheck: false }}
/>
<ChecklistItem
label="Search only data fields"
id={"search-data-field-facet"}
checked
inputProps={{
disabled: true,
}}
onChange={() => {}}
inputProps={{ disabled: true }}
onChange={() => { }}
/>
<Box py={3}>
{results ? <SearchResults results={results} /> : "Loading..."}
<Box pt={3}>
{results.length > 0 &&
<>
<SearchResults results={results} />
<ExternalPortalList />
</>
}
</Box>
</Container>
);
Expand Down
Loading

0 comments on commit e64dec4

Please sign in to comment.