diff --git a/editor.planx.uk/src/lib/featureFlags.ts b/editor.planx.uk/src/lib/featureFlags.ts
index 31cbfdf23c..90a52dfe3f 100644
--- a/editor.planx.uk/src/lib/featureFlags.ts
+++ b/editor.planx.uk/src/lib/featureFlags.ts
@@ -1,5 +1,5 @@
// add/edit/remove feature flags in array below
-const AVAILABLE_FEATURE_FLAGS = [] as const;
+const AVAILABLE_FEATURE_FLAGS = ["SEARCH"] as const;
type FeatureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number];
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx
new file mode 100644
index 0000000000..679377c8cb
--- /dev/null
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search.tsx
@@ -0,0 +1,134 @@
+import Box from "@mui/material/Box";
+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 { 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 {
+ nodeId: string;
+ nodeType: ComponentType;
+ nodeTitle?: string;
+ text: string;
+ path: string[];
+}
+
+const SearchResults: React.FC<{ results: SearchResult[] }> = ({ results }) => {
+ return (
+
+ {results.map((result) => (
+
+ ))}
+
+ );
+};
+
+const SearchResultCard: React.FC = ({
+ nodeTitle,
+ text,
+ nodeType,
+}) => {
+ const Icon = ICONS[nodeType];
+
+ return (
+ ({
+ pb: 2,
+ borderBottom: `1px solid ${theme.palette.border.main}`,
+ })}
+ >
+
+ {Icon && }
+
+ Question
+ {nodeTitle && ` - ${nodeTitle}`}
+
+
+ {text}
+
+ );
+};
+
+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 handleChange = (e: ChangeEvent) => {
+ const input = e.target.value;
+ setQuery(input);
+ debounceSearch(input);
+ };
+
+ 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(
+ debouncedQuery ? `${endpoint}?find=${debouncedQuery}` : null,
+ fetcher,
+ );
+
+ return (
+
+
+
+
+ {}}
+ />
+
+ {error && "Something went wrong"}
+ {mockData ? : "Loading..."}
+
+
+ );
+};
+
+export default Search;
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx
index 1cc8df1652..ed5ed9ab49 100644
--- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx
@@ -19,6 +19,7 @@ import Tabs from "@mui/material/Tabs";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { AxiosError } from "axios";
+import { hasFeatureFlag } from "lib/featureFlags";
import { formatLastPublishMessage } from "pages/FlowEditor/utils";
import React, { useState } from "react";
import { useAsync } from "react-use";
@@ -34,8 +35,9 @@ import {
ValidationCheck,
ValidationChecks,
} from "./PublishDialog";
+import Search from "./Search";
-type SidebarTabs = "PreviewBrowser" | "History";
+type SidebarTabs = "PreviewBrowser" | "History" | "Search";
const Console = styled(Box)(() => ({
overflow: "auto",
@@ -449,6 +451,15 @@ const Sidebar: React.FC<{
value="History"
label="History"
/>
+ {hasFeatureFlag("SEARCH") && (
+
+ )}
{activeTab === "PreviewBrowser" && (
@@ -463,6 +474,11 @@ const Sidebar: React.FC<{
)}
+ {activeTab === "Search" && (
+
+
+
+ )}
{showDebugConsole && }
);
diff --git a/editor.planx.uk/src/ui/shared/Checkbox.tsx b/editor.planx.uk/src/ui/shared/Checkbox.tsx
index db2fe034f2..90e816b41b 100644
--- a/editor.planx.uk/src/ui/shared/Checkbox.tsx
+++ b/editor.planx.uk/src/ui/shared/Checkbox.tsx
@@ -1,9 +1,11 @@
-import Box from "@mui/material/Box";
+import Box, { BoxProps } from "@mui/material/Box";
import { styled } from "@mui/material/styles";
import React from "react";
import { borderedFocusStyle } from "theme";
-const Root = styled(Box)(({ theme }) => ({
+const Root = styled(Box, {
+ shouldForwardProp: (prop) => !["disabled"].includes(prop.toString()),
+})(({ theme, disabled }) => ({
display: "inline-flex",
flexShrink: 0,
position: "relative",
@@ -13,6 +15,10 @@ const Root = styled(Box)(({ theme }) => ({
border: "2px solid",
backgroundColor: theme.palette.common.white,
"&:focus-within": borderedFocusStyle,
+ ...(disabled && {
+ border: `2px solid ${theme.palette.grey[400]}`,
+ backgroundColor: theme.palette.grey[400],
+ }),
}));
const Input = styled("input")(() => ({
@@ -24,11 +30,13 @@ const Input = styled("input")(() => ({
interface IconProps extends React.HTMLAttributes {
checked: boolean;
+ disabled?: boolean;
}
const Icon = styled("span", {
- shouldForwardProp: (prop) => prop !== "checked",
-})(({ theme, checked }) => ({
+ shouldForwardProp: (prop) =>
+ !["checked", "disabled"].includes(prop.toString()),
+})(({ theme, checked, disabled }) => ({
display: checked ? "block" : "none",
content: "''",
position: "absolute",
@@ -41,6 +49,10 @@ const Icon = styled("span", {
top: "42%",
transform: "translate(-50%, -50%) rotate(45deg)",
cursor: "pointer",
+ ...(disabled && {
+ borderBottom: `5px solid white`,
+ borderRight: `5px solid white`,
+ }),
}));
export interface Props {
@@ -62,7 +74,7 @@ export default function Checkbox({
};
return (
-
+
-
+
);
}
diff --git a/editor.planx.uk/src/ui/shared/ChecklistItem.tsx b/editor.planx.uk/src/ui/shared/ChecklistItem.tsx
index 85f1c90674..b1cc24e8a3 100644
--- a/editor.planx.uk/src/ui/shared/ChecklistItem.tsx
+++ b/editor.planx.uk/src/ui/shared/ChecklistItem.tsx
@@ -26,6 +26,7 @@ interface Props {
label: string;
checked: boolean;
onChange: (event?: React.MouseEvent) => void;
+ inputProps?: React.InputHTMLAttributes;
}
export default function ChecklistItem({
@@ -33,10 +34,16 @@ export default function ChecklistItem({
onChange,
checked,
id,
+ inputProps,
}: Props): FCReturn {
return (
-
+