From 5e02b80d522367075f693a5d0e78aeaa4d8cb289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 27 Aug 2024 14:51:52 +0100 Subject: [PATCH 1/8] chore: Update folder structure --- .../Search/{SearchResultCard.tsx => SearchResultCard/index.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/{SearchResultCard.tsx => SearchResultCard/index.tsx} (98%) 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/index.tsx similarity index 98% rename from editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx rename to editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx index 30f5621d2a..1c9a95a8ca 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx @@ -11,7 +11,7 @@ import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; import { FONT_WEIGHT_SEMI_BOLD } from "theme"; -import { Headline } from "./Headline"; +import { Headline } from "../Headline"; export const Root = styled(ListItemButton)(({ theme }) => ({ padding: theme.spacing(1), From 39fb9c265be81b0b5606e8da04d97f5c0fe02cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 07:24:14 +0100 Subject: [PATCH 2/8] feat: Use a map data structure to hold dispay data for search cards, for each component variant --- editor.planx.uk/src/hooks/useSearch.ts | 22 ++-- .../SearchResultCard/DataDisplayMap.tsx | 114 ++++++++++++++++++ .../Sidebar/Search/SearchResultCard/index.tsx | 45 ++----- .../components/Sidebar/Search/mocks/simple.ts | 2 + 4 files changed, 137 insertions(+), 46 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx diff --git a/editor.planx.uk/src/hooks/useSearch.ts b/editor.planx.uk/src/hooks/useSearch.ts index 16dbe22f37..a186fe707f 100644 --- a/editor.planx.uk/src/hooks/useSearch.ts +++ b/editor.planx.uk/src/hooks/useSearch.ts @@ -9,7 +9,8 @@ interface UseSearchProps { export interface SearchResult { item: T; key: string; - matchIndices?: [number, number][]; + matchIndices: [number, number][]; + refIndex: number; } export type SearchResults = SearchResult[]; @@ -39,13 +40,18 @@ export const useSearch = ({ useEffect(() => { const fuseResults = fuse.search(pattern); 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, - })), + fuseResults.map((result) => { + // Required type narrowing for FuseResult + if (!result.matches) throw Error("Matches missing from FuseResults"); + + return { + item: result.item, + key: result.matches?.[0].key || "", + // We only display the first match + matchIndices: result.matches[0].indices as [number, number][], + refIndex: result.matches[0]?.refIndex || 0, + }; + }), ); }, [pattern, fuse]); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx new file mode 100644 index 0000000000..34a7395bcf --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx @@ -0,0 +1,114 @@ +import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { SearchResult } from "hooks/useSearch"; +import { capitalize, get } from "lodash"; +import { SLUGS } from "pages/FlowEditor/data/types"; +import { useStore } from "pages/FlowEditor/lib/store"; + +interface DataDisplayValues { + displayKey: string; + getIconKey: (result: SearchResult) => ComponentType; + getTitle: (result: SearchResult) => string; + getHeadline: (result: SearchResult) => string; + getComponentType: (result: SearchResult) => string; +} + +/** + * Map of data keys to their associated display values + * Uses Partial as not all values are unique, we later apply defaults + */ +type DataKeyMap = Record>; + +/** + * Map of ComponentTypes to their associated data keys + */ +type ComponentMap = Record; + +/** + * Map of ComponentTypes which need specific overrides in order to display their data values + */ +const DISPLAY_DATA: Partial = { + // Answers are mapped to their parent questions + [ComponentType.Answer]: { + default: { + getIconKey: () => ComponentType.Question, + displayKey: "Option (data)", + getTitle: ({ item }) => { + const parentNode = useStore.getState().flow[item.parentId]; + return parentNode!.data.text!; + }, + getHeadline: ({ item, key }) => get(item, key)?.toString(), + }, + }, + // FileUploadAndLabel has data values nested in FileTypes + [ComponentType.FileUploadAndLabel]: { + default: { + displayKey: "File type (data)", + getHeadline: ({ item, refIndex }) => + (item["data"]?.["fileTypes"] as [])[refIndex]["fn"], + }, + }, + // Calculate contains both input and output data values + [ComponentType.Calculate]: { + formula: { + displayKey: "Formula", + getHeadline: ({ item }) => item.data!.formula as string, + }, + "data.output": { + displayKey: "Output (data)", + getHeadline: ({ item }) => item.data!.output as string, + }, + }, + // List contains data variables nested within its schema + [ComponentType.List]: { + "data.schema.fields.data.fn": { + getHeadline: ({ item, refIndex }) => { + // TODO: Add type guards, remove "as" + return (item.data as any).schema.fields[refIndex].data.fn; + }, + }, + "data.schema.fields.data.options.data.val": { + displayKey: "Option (data)", + getHeadline: ({ item, refIndex }) => { + // Fuse.js flattens deeply nested arrays when using refIndex + // TODO: Add type guards, remove "as" + const options = (item.data as any).schema.fields.flatMap( + (field: any) => field.data.options, + ); + return options[refIndex].data.val; + }, + }, + }, +}; + +/** + * Default values for all ComponentTypes not listed in DISPLAY_DATA + */ +const DEFAULT_DISPLAY_DATA: DataDisplayValues = { + displayKey: "Data", + getIconKey: ({ item }) => item.type, + getTitle: ({ item }) => + (item.data?.title as string) || (item.data?.text as string) || "", + getHeadline: ({ item, key }) => get(item, key)?.toString() || "", + getComponentType: ({ item }) => + capitalize(SLUGS[item.type].replaceAll("-", " ")), +}; + +export const getDisplayDetailsForResult = ( + result: SearchResult, +) => { + const componentMap = DISPLAY_DATA[result.item.type]; + const keyMap = componentMap?.[result.key] || componentMap?.default || {}; + + const data: DataDisplayValues = { + ...DEFAULT_DISPLAY_DATA, + ...keyMap, + }; + + return { + iconKey: data.getIconKey(result), + componentType: data.getComponentType(result), + title: data.getTitle(result), + key: data.displayKey, + headline: data.getHeadline(result), + }; +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx index 1c9a95a8ca..8478f41819 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx @@ -2,16 +2,14 @@ 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 { 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"; +import { getDisplayDetailsForResult } from "./DataDisplayMap"; export const Root = styled(ListItemButton)(({ theme }) => ({ padding: theme.spacing(1), @@ -22,42 +20,13 @@ export const Root = styled(ListItemButton)(({ theme }) => ({ 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 + // TODO - display portal wrapper + const { iconKey, componentType, title, key, headline } = + getDisplayDetailsForResult(result); + const Icon = ICONS[iconKey]; const handleClick = () => { - console.log("todo!"); - console.log({ nodeId: result.item.id }); + console.log({ result }); // get path for node // generate url from path // navigate to url diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts index 275c397f34..391b4bec02 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts @@ -49,6 +49,7 @@ export const results: SearchResults = [ }, key: "data.val", matchIndices: [[0, 2]], + refIndex: 0, }, { item: { @@ -62,5 +63,6 @@ export const results: SearchResults = [ }, key: "data.val", matchIndices: [[0, 2]], + refIndex: 0, }, ]; From 20f45723b1be500900d3b42bf0b85cd5643ee4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 07:46:38 +0100 Subject: [PATCH 3/8] test: Setup todos --- .../SearchResultCard/DataDisplayMap.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts new file mode 100644 index 0000000000..5d2300a9fa --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts @@ -0,0 +1,22 @@ +describe("Question component", () => { + it.todo("returns the expected display values"); +}); + +describe("Answer component", () => { + it.todo("returns the expected display values"); +}); + +describe("List component", () => { + it.todo("handles the default (root) data value"); + it.todo("handles nested data variables"); + it.todo("handles nested data variables in Answers"); +}); + +describe("Calculate component", () => { + it.todo("handles the output data variables"); + it.todo("handles the formula data variables"); +}); + +describe("FileUploadAndLabel component", () => { + it.todo("handles the data variables nested in FileTypes"); +}); From 790d9efaedb617f5da248c2116319a1a6f2bfa28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 10:59:53 +0100 Subject: [PATCH 4/8] test: Coverage for all exceptional node types --- .../SearchResultCard/DataDisplayMap.test.ts | 120 +++- .../Sidebar/Search/mocks/DataDisplayMap.ts | 679 ++++++++++++++++++ 2 files changed, 791 insertions(+), 8 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts index 5d2300a9fa..d8e07894e3 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts @@ -1,22 +1,126 @@ +import { ComponentType } from "@opensystemslab/planx-core/types"; +import { useStore } from "pages/FlowEditor/lib/store"; + +import { + mockAnswerResult, + mockCalculateFormulaResult, + mockCalculateRootResult, + mockFileUploadAndLabelResult, + mockFlow, + mockListAnswerResult, + mockListDataResult, + mockListRootResult, + mockQuestionResult, +} from "../mocks/DataDisplayMap"; +import { getDisplayDetailsForResult } from "./DataDisplayMap"; + +type Output = ReturnType; + +// Setup flow so that it can be referenced by SearchResults (e.g. getting parent nodes) +beforeAll(() => useStore.setState({ flow: mockFlow })); + describe("Question component", () => { - it.todo("returns the expected display values"); + it("returns the expected display values", () => { + const output = getDisplayDetailsForResult(mockQuestionResult); + + expect(output).toStrictEqual({ + key: "Data", + iconKey: ComponentType.Question, + componentType: "Question", + title: "This is a question component", + headline: "colour", + }); + }); }); describe("Answer component", () => { - it.todo("returns the expected display values"); + it("returns the expected display values", () => { + const output = getDisplayDetailsForResult(mockAnswerResult); + + expect(output).toStrictEqual({ + key: "Option (data)", + iconKey: ComponentType.Question, + componentType: "Question", + title: "This is a question component", + headline: "red", + }); + }); }); describe("List component", () => { - it.todo("handles the default (root) data value"); - it.todo("handles nested data variables"); - it.todo("handles nested data variables in Answers"); + it("handles the root data value", () => { + const output = getDisplayDetailsForResult(mockListRootResult); + + expect(output).toStrictEqual({ + componentType: "List", + headline: "listRoot", + iconKey: ComponentType.List, + key: "Data", + title: "This is a list component", + }); + }); + + it("handles nested data variables", () => { + const output = getDisplayDetailsForResult(mockListDataResult); + + expect(output).toStrictEqual({ + componentType: "List", + headline: "tenure", + iconKey: ComponentType.List, + key: "Data", + title: "This is a list component", + }); + }); + + it("handles nested data variables in Answers", () => { + const output = getDisplayDetailsForResult(mockListAnswerResult); + + expect(output).toStrictEqual({ + componentType: "List", + headline: "selfCustomBuild", + iconKey: ComponentType.List, + key: "Option (data)", + title: "This is a list component", + }); + }); }); describe("Calculate component", () => { - it.todo("handles the output data variables"); - it.todo("handles the formula data variables"); + it("handles the output data variables", () => { + const output = getDisplayDetailsForResult(mockCalculateRootResult); + + expect(output).toStrictEqual({ + componentType: "Calculate", + headline: "calculateOutput", + iconKey: ComponentType.Calculate, + key: "Output (data)", + title: "This is a calculate component", + }); + }); + + it("handles the formula data variables", () => { + const output = getDisplayDetailsForResult(mockCalculateFormulaResult); + + expect(output).toStrictEqual({ + componentType: "Calculate", + headline: "formulaOne + formulaTwo", + iconKey: ComponentType.Calculate, + key: "Formula", + title: "This is a calculate component", + }); + }); }); describe("FileUploadAndLabel component", () => { - it.todo("handles the data variables nested in FileTypes"); + it("handles the data variables nested in FileTypes", () => { + const output = getDisplayDetailsForResult(mockFileUploadAndLabelResult); + + expect(output).toStrictEqual({ + componentType: "File upload and label", + headline: "floorplan", + iconKey: ComponentType.FileUploadAndLabel, + key: "File type (data)", + title: "This is a FileUploadAndLabel component", + }); + }); }); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts new file mode 100644 index 0000000000..44f4efeb23 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts @@ -0,0 +1,679 @@ +import { FlowGraph, IndexedNode } from "@opensystemslab/planx-core/types"; + +import { SearchResult } from "../../../../../../hooks/useSearch"; + +/** Simple flow which contains an example of each component which has unique rules for finding data values and displaying these as search results */ +export const mockFlow: FlowGraph = { + _root: { + edges: ["UMJi4q9zud", "Xj4E14wvd6", "zryBH8H7vD", "Flfg7UnuhH"], + }, + "3W0WyymBuj": { + data: { + val: "blue", + text: "Blue", + }, + type: 200, + }, + Flfg7UnuhH: { + data: { + title: "This is a FileUploadAndLabel component", + fileTypes: [ + { + fn: "floorplan", + name: "Floorplan", + rule: { + condition: "AlwaysRequired", + }, + }, + ], + hideDropZone: false, + }, + type: 145, + }, + UMJi4q9zud: { + data: { + fn: "colour", + text: "This is a question component", + }, + type: 100, + edges: ["th2EEQ03a7", "3W0WyymBuj"], + }, + Xj4E14wvd6: { + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Existing residential unit type", + fields: [ + { + data: { + fn: "type", + title: "What best describes the type of this unit?", + options: [ + { + id: "house", + data: { + val: "house", + text: "House", + }, + }, + { + id: "flat", + data: { + val: "flat", + text: "Flat, apartment or maisonette", + }, + }, + { + id: "sheltered", + data: { + val: "sheltered", + text: "Sheltered housing", + }, + }, + { + id: "studio", + data: { + val: "studio", + text: "Studio or bedsit", + }, + }, + { + id: "cluster", + data: { + val: "cluster", + text: "Cluster flat", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "tenure", + title: "What best describes the tenure of this unit?", + options: [ + { + id: "MH", + data: { + val: "MH", + text: "Market housing", + }, + }, + { + id: "SAIR", + data: { + val: "SAIR", + text: "Social, affordable or interim rent", + }, + }, + { + id: "AHO", + data: { + val: "AHO", + text: "Affordable home ownership", + }, + }, + { + id: "SH", + data: { + val: "SH", + text: "Starter homes", + }, + }, + { + id: "selfCustomBuild", + data: { + val: "selfCustomBuild", + text: "Self-build and custom build", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "bedrooms", + title: "How many bedrooms does this unit have?", + allowNegatives: false, + }, + type: "number", + }, + { + data: { + fn: "identicalUnits", + title: + "How many units of the type described above exist on the site?", + allowNegatives: false, + }, + type: "number", + }, + ], + }, + schemaName: "Residential units - Existing", + }, + type: 800, + }, + th2EEQ03a7: { + data: { + val: "red", + text: "Red", + }, + type: 200, + }, + zryBH8H7vD: { + data: { + title: "This is a calculate component", + output: "calculateOutput", + formula: "formulaOne + formulaTwo", + defaults: { + formulaOne: "1", + formulaTwo: "1", + }, + formatOutputForAutomations: false, + samples: {}, + }, + type: 700, + }, +}; + +export const mockQuestionResult: SearchResult = { + item: { + id: "UMJi4q9zud", + parentId: "_root", + type: 100, + edges: ["th2EEQ03a7", "3W0WyymBuj"], + data: { + fn: "colour", + text: "This is a question component", + }, + }, + key: "data.fn", + matchIndices: [[0, 3]], + refIndex: 0, +}; + +export const mockAnswerResult: SearchResult = { + item: { + id: "th2EEQ03a7", + parentId: "UMJi4q9zud", + type: 200, + data: { + text: "Red", + val: "red", + }, + }, + key: "data.val", + matchIndices: [[0, 2]], + refIndex: 0, +}; + +export const mockListRootResult: SearchResult = { + item: { + id: "Xj4E14wvd6", + parentId: "_root", + type: 800, + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Tree type", + fields: [ + { + data: { + fn: "species", + type: "short", + title: "Species", + }, + type: "text", + }, + { + data: { + fn: "work", + type: "short", + title: "Proposed work", + }, + type: "text", + }, + { + data: { + fn: "justification", + type: "short", + title: "Justification", + }, + type: "text", + }, + { + data: { + fn: "urgency", + title: "Urgency", + options: [ + { + id: "low", + data: { + val: "low", + text: "Low", + }, + }, + { + id: "moderate", + data: { + val: "moderate", + text: "Moderate", + }, + }, + { + id: "high", + data: { + val: "high", + text: "High", + }, + }, + { + id: "urgent", + data: { + val: "urgent", + text: "Urgent", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "completionDate", + title: "Expected completion date", + }, + type: "date", + }, + { + data: { + fn: "features", + title: "Where is it? Plot as many as apply", + mapOptions: { + basemap: "MapboxSatellite", + drawMany: true, + drawType: "Point", + drawColor: "#66ff00", + }, + }, + type: "map", + }, + ], + }, + schemaName: "Trees", + }, + }, + key: "data.fn", + matchIndices: [[0, 7]], + refIndex: 0, +}; + +export const mockListDataResult: SearchResult = { + item: { + id: "Xj4E14wvd6", + parentId: "_root", + type: 800, + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Existing residential unit type", + fields: [ + { + data: { + fn: "type", + title: "What best describes the type of this unit?", + options: [ + { + id: "house", + data: { + val: "house", + text: "House", + }, + }, + { + id: "flat", + data: { + val: "flat", + text: "Flat, apartment or maisonette", + }, + }, + { + id: "sheltered", + data: { + val: "sheltered", + text: "Sheltered housing", + }, + }, + { + id: "studio", + data: { + val: "studio", + text: "Studio or bedsit", + }, + }, + { + id: "cluster", + data: { + val: "cluster", + text: "Cluster flat", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "tenure", + title: "What best describes the tenure of this unit?", + options: [ + { + id: "MH", + data: { + val: "MH", + text: "Market housing", + }, + }, + { + id: "SAIR", + data: { + val: "SAIR", + text: "Social, affordable or interim rent", + }, + }, + { + id: "AHO", + data: { + val: "AHO", + text: "Affordable home ownership", + }, + }, + { + id: "SH", + data: { + val: "SH", + text: "Starter homes", + }, + }, + { + id: "selfCustomBuild", + data: { + val: "selfCustomBuild", + text: "Self-build and custom build", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "bedrooms", + title: "How many bedrooms does this unit have?", + allowNegatives: false, + }, + type: "number", + }, + { + data: { + fn: "identicalUnits", + title: + "How many units of the type described above exist on the site?", + allowNegatives: false, + }, + type: "number", + }, + ], + }, + schemaName: "Residential units - Existing", + }, + }, + key: "data.schema.fields.data.fn", + matchIndices: [[0, 5]], + refIndex: 1, +}; + +export const mockListAnswerResult: SearchResult = { + item: { + id: "Xj4E14wvd6", + parentId: "_root", + type: 800, + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Existing residential unit type", + fields: [ + { + data: { + fn: "type", + title: "What best describes the type of this unit?", + options: [ + { + id: "house", + data: { + val: "house", + text: "House", + }, + }, + { + id: "flat", + data: { + val: "flat", + text: "Flat, apartment or maisonette", + }, + }, + { + id: "sheltered", + data: { + val: "sheltered", + text: "Sheltered housing", + }, + }, + { + id: "studio", + data: { + val: "studio", + text: "Studio or bedsit", + }, + }, + { + id: "cluster", + data: { + val: "cluster", + text: "Cluster flat", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "tenure", + title: "What best describes the tenure of this unit?", + options: [ + { + id: "MH", + data: { + val: "MH", + text: "Market housing", + }, + }, + { + id: "SAIR", + data: { + val: "SAIR", + text: "Social, affordable or interim rent", + }, + }, + { + id: "AHO", + data: { + val: "AHO", + text: "Affordable home ownership", + }, + }, + { + id: "SH", + data: { + val: "SH", + text: "Starter homes", + }, + }, + { + id: "selfCustomBuild", + data: { + val: "selfCustomBuild", + text: "Self-build and custom build", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "bedrooms", + title: "How many bedrooms does this unit have?", + allowNegatives: false, + }, + type: "number", + }, + { + data: { + fn: "identicalUnits", + title: + "How many units of the type described above exist on the site?", + allowNegatives: false, + }, + type: "number", + }, + ], + }, + schemaName: "Residential units - Existing", + }, + }, + key: "data.schema.fields.data.options.data.val", + matchIndices: [[0, 14]], + refIndex: 10, +}; + +export const mockCalculateRootResult: SearchResult = { + item: { + id: "zryBH8H7vD", + parentId: "_root", + type: 700, + data: { + title: "This is a calculate component", + output: "calculateOutput", + formula: "formulaOne + formulaTwo", + defaults: { + formulaOne: "1", + formulaTwo: "1", + }, + formatOutputForAutomations: false, + samples: {}, + }, + }, + key: "data.output", + matchIndices: [[0, 14]], + refIndex: 0, +}; + +export const mockCalculateFormulaResult: SearchResult = { + item: { + id: "zryBH8H7vD", + parentId: "_root", + type: 700, + data: { + title: "This is a calculate component", + output: "calculateOutput", + formula: "formulaOne + formulaTwo", + defaults: { + formulaOne: "1", + formulaTwo: "1", + }, + formatOutputForAutomations: false, + samples: {}, + }, + }, + key: "formula", + matchIndices: [[0, 6]], + refIndex: 1, +}; + +export const mockFileUploadAndLabelResult: SearchResult = { + item: { + id: "Flfg7UnuhH", + parentId: "_root", + type: 145, + data: { + title: "This is a FileUploadAndLabel component", + fileTypes: [ + { + fn: "floorplan", + name: "Floorplan", + rule: { + condition: "AlwaysRequired", + }, + }, + ], + hideDropZone: false, + }, + }, + key: "data.fileTypes.fn", + matchIndices: [[0, 8]], + refIndex: 0, +}; From 391b4b854c06c8d4f4d77175971f86c2df7ae238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 11:00:32 +0100 Subject: [PATCH 5/8] chore: Drop filter which hid excpetional nodes --- .../Sidebar/Search/NodeSearchResults.tsx | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) 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 index c8dfedceaf..7672884536 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx @@ -2,7 +2,7 @@ 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 { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { IndexedNode } from "@opensystemslab/planx-core/types"; import type { SearchResults } from "hooks/useSearch"; import React from "react"; @@ -17,32 +17,20 @@ export const Root = styled(List)(({ theme }) => ({ export const NodeSearchResults: React.FC<{ results: SearchResults; -}> = ({ results }) => { - /** Temporary guard function to filter out component types not yet supported by SearchResultCard */ - const isSupportedNodeType = ( - result: SearchResults[number], - ): boolean => - ![ - ComponentType.FileUploadAndLabel, - ComponentType.Calculate, - ComponentType.List, - ].includes(result.item.type); +}> = ({ results }) => ( + <> + + {!results.length && "No matches found"} + {results.length === 1 && "1 result:"} + {results.length > 1 && `${results.length} results:`} + - return ( - <> - - {!results.length && "No matches found"} - {results.length === 1 && "1 result:"} - {results.length > 1 && `${results.length} results:`} - - - - {results.filter(isSupportedNodeType).map((result) => ( - - - - ))} - - - ); -}; + + {results.map((result) => ( + + + + ))} + + +); From 30ea8e8369fb771c8b00726e2a5b096781ffca11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 11:31:26 +0100 Subject: [PATCH 6/8] chore: Slightly better types --- .../SearchResultCard/DataDisplayMap.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx index 34a7395bcf..ea0b2b4953 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx @@ -1,4 +1,7 @@ import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { Calculate } from "@planx/components/Calculate/model"; +import { FileUploadAndLabel } from "@planx/components/FileUploadAndLabel/model"; +import { List } from "@planx/components/List/model"; import { SearchResult } from "hooks/useSearch"; import { capitalize, get } from "lodash"; import { SLUGS } from "pages/FlowEditor/data/types"; @@ -34,7 +37,7 @@ const DISPLAY_DATA: Partial = { displayKey: "Option (data)", getTitle: ({ item }) => { const parentNode = useStore.getState().flow[item.parentId]; - return parentNode!.data.text!; + return parentNode.data.text; }, getHeadline: ({ item, key }) => get(item, key)?.toString(), }, @@ -44,37 +47,36 @@ const DISPLAY_DATA: Partial = { default: { displayKey: "File type (data)", getHeadline: ({ item, refIndex }) => - (item["data"]?.["fileTypes"] as [])[refIndex]["fn"], + (item["data"] as unknown as FileUploadAndLabel)["fileTypes"][refIndex][ + "fn" + ], }, }, // Calculate contains both input and output data values [ComponentType.Calculate]: { formula: { displayKey: "Formula", - getHeadline: ({ item }) => item.data!.formula as string, + getHeadline: ({ item }) => (item.data as unknown as Calculate).formula, }, "data.output": { displayKey: "Output (data)", - getHeadline: ({ item }) => item.data!.output as string, + getHeadline: ({ item }) => (item.data as unknown as Calculate).output, }, }, // List contains data variables nested within its schema [ComponentType.List]: { "data.schema.fields.data.fn": { - getHeadline: ({ item, refIndex }) => { - // TODO: Add type guards, remove "as" - return (item.data as any).schema.fields[refIndex].data.fn; - }, + getHeadline: ({ item, refIndex }) => + (item.data as unknown as List).schema.fields[refIndex].data.fn, }, "data.schema.fields.data.options.data.val": { displayKey: "Option (data)", getHeadline: ({ item, refIndex }) => { // Fuse.js flattens deeply nested arrays when using refIndex - // TODO: Add type guards, remove "as" - const options = (item.data as any).schema.fields.flatMap( - (field: any) => field.data.options, - ); - return options[refIndex].data.val; + const options = (item.data as unknown as List).schema.fields + .filter((field) => field.type === "question") + .flatMap((field) => field.data.options); + return options[refIndex].data.val || ""; }, }, }, From 7234ca858c8de09f0cd7bb726d9f0bace6a08bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 12:06:41 +0100 Subject: [PATCH 7/8] fix: Add temporary type guard --- .../Sidebar/Search/SearchResultCard/DataDisplayMap.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx index ea0b2b4953..74c30f623c 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx @@ -2,6 +2,7 @@ import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; import { Calculate } from "@planx/components/Calculate/model"; import { FileUploadAndLabel } from "@planx/components/FileUploadAndLabel/model"; import { List } from "@planx/components/List/model"; +import { QuestionField } from "@planx/components/shared/Schema/model"; import { SearchResult } from "hooks/useSearch"; import { capitalize, get } from "lodash"; import { SLUGS } from "pages/FlowEditor/data/types"; @@ -74,7 +75,8 @@ const DISPLAY_DATA: Partial = { getHeadline: ({ item, refIndex }) => { // Fuse.js flattens deeply nested arrays when using refIndex const options = (item.data as unknown as List).schema.fields - .filter((field) => field.type === "question") + // TODO: Remove type guard when bumping to TS v5.5 + .filter((field): field is QuestionField => field.type === "question") .flatMap((field) => field.data.options); return options[refIndex].data.val || ""; }, From 78be4194a1eed500229431dbfdc8f64007454ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 16:55:39 +0100 Subject: [PATCH 8/8] chore: Drop type guard after TS bump --- .../Sidebar/Search/SearchResultCard/DataDisplayMap.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx index 74c30f623c..ea0b2b4953 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx @@ -2,7 +2,6 @@ import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; import { Calculate } from "@planx/components/Calculate/model"; import { FileUploadAndLabel } from "@planx/components/FileUploadAndLabel/model"; import { List } from "@planx/components/List/model"; -import { QuestionField } from "@planx/components/shared/Schema/model"; import { SearchResult } from "hooks/useSearch"; import { capitalize, get } from "lodash"; import { SLUGS } from "pages/FlowEditor/data/types"; @@ -75,8 +74,7 @@ const DISPLAY_DATA: Partial = { getHeadline: ({ item, refIndex }) => { // Fuse.js flattens deeply nested arrays when using refIndex const options = (item.data as unknown as List).schema.fields - // TODO: Remove type guard when bumping to TS v5.5 - .filter((field): field is QuestionField => field.type === "question") + .filter((field) => field.type === "question") .flatMap((field) => field.data.options); return options[refIndex].data.val || ""; },