From 77983b66fe2b334ffaa4c9d72c1b5f8d80fad686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 8 Oct 2024 12:32:56 +0100 Subject: [PATCH 1/6] feat: Strip HTML tags from search results --- editor.planx.uk/src/hooks/useSearch.ts | 7 +++ .../getDisplayDetailsForResult.tsx | 4 +- .../components/Sidebar/Search/facets.ts | 59 +++++++++++++------ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/editor.planx.uk/src/hooks/useSearch.ts b/editor.planx.uk/src/hooks/useSearch.ts index a186fe707f..a33f4e764c 100644 --- a/editor.planx.uk/src/hooks/useSearch.ts +++ b/editor.planx.uk/src/hooks/useSearch.ts @@ -7,9 +7,15 @@ interface UseSearchProps { } export interface SearchResult { + /** Original indexed item */ item: T; + /** Key used to locate value to search against */ key: string; + /** Indices within searched string that match search term */ matchIndices: [number, number][]; + /** String matched against - does not necessarily equate to item[key] */ + matchValue: string; + /** Index within flattened array of item[key] */ refIndex: number; } @@ -49,6 +55,7 @@ export const useSearch = ({ key: result.matches?.[0].key || "", // We only display the first match matchIndices: result.matches[0].indices as [number, number][], + matchValue: result.matches[0].value!, refIndex: result.matches[0]?.refIndex || 0, }; }), diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx index ed33d2a4a1..86dfa158c5 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx @@ -13,7 +13,7 @@ import { Pay } from "@planx/components/Pay/model"; import { Schema } from "@planx/components/shared/Schema/model"; import { TaskList } from "@planx/components/TaskList/model"; import { SearchResult } from "hooks/useSearch"; -import { capitalize, get } from "lodash"; +import { capitalize } from "lodash"; import { SLUGS } from "pages/FlowEditor/data/types"; import { useStore } from "pages/FlowEditor/lib/store"; @@ -307,7 +307,7 @@ const defaultFormatter: SearchResultFormatter = { getIconKey: ({ item }) => item.type, getTitle: ({ item }) => (item.data?.title as string) || (item.data?.text as string) || "", - getHeadline: ({ item, key }) => get(item, key)?.toString() || "", + getHeadline: ({ matchValue }) => matchValue, getComponentType: ({ item }) => capitalize(SLUGS[item.type].replaceAll("-", " ")), }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts index c99341438c..0a163b40bc 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts @@ -1,5 +1,8 @@ import { flatFlags, IndexedNode } from "@opensystemslab/planx-core/types"; -import { FuseOptionKey } from "fuse.js"; +import { FuseOptionKey, FuseOptionKeyObject } from "fuse.js"; +import { get } from "lodash"; + +import { stripTagsAndLimitLength } from "../../Flow/lib/utils"; export type SearchFacets = Array>; @@ -34,32 +37,46 @@ export const DATA_FACETS: SearchFacets = [ ...drawBoundaryData, ]; +/** + * Generate a Fuse getFn in order to search against the text content of the HTML generated by RichTextInput fields + * Strips HTML tags from searched value in order to maintain matchIndices + * Docs: https://www.fusejs.io/examples.html#nested-search + */ +const richTextField = ( + key: `data.${string}`, +): FuseOptionKeyObject => ({ + name: key, + getFn: (node: IndexedNode) => + stripTagsAndLimitLength(get(node as Record, key) || "", ""), +}); + const basicFields: SearchFacets = [ "data.text", "data.title", - "data.description", + richTextField("data.description"), ]; const moreInformation: SearchFacets = [ "data.notes", - "data.howMeasured", - "data.policyRef", - "data.info", + richTextField("data.howMeasured"), + richTextField("data.policyRef"), + richTextField("data.info"), ]; const checklist: SearchFacets = ["data.categories.title"]; const nextSteps: SearchFacets = [ "data.steps.title", - "data.steps.description", + richTextField("data.steps.description"), "data.steps.url", ]; const fileUploadAndLabel: SearchFacets = [ "data.fileTypes.name", - "data.fileTypes.moreInformation.howMeasured", - "data.fileTypes.moreInformation.policyRef", - "data.fileTypes.moreInformation.info", + "data.fileTypes.moreInformation.notes", + richTextField("data.fileTypes.moreInformation.howMeasured"), + richTextField("data.fileTypes.moreInformation.policyRef"), + richTextField("data.fileTypes.moreInformation.info"), ]; const numberInput: SearchFacets = ["data.units"]; @@ -74,10 +91,14 @@ const schemaComponents: SearchFacets = [ // Option title "data.schema.fields.data.options.data.text", // Option description + // Currently just string - could be rich text once we have an Editor interface for generating schemas "data.schema.fields.data.options.data.description", ]; -const taskList: SearchFacets = ["data.tasks.title", "data.tasks.description"]; +const taskList: SearchFacets = [ + "data.tasks.title", + richTextField("data.tasks.description"), +]; const result: SearchFacets = [ ...flatFlags.flatMap(({ value }) => [ @@ -86,38 +107,38 @@ const result: SearchFacets = [ ]), ]; -const content: SearchFacets = ["data.content"]; +const content: SearchFacets = [richTextField("data.content")]; const confirmation: SearchFacets = [ "data.heading", - "data.moreInfo", - "data.contactInfo", + richTextField("data.moreInfo"), + richTextField("data.contactInfo"), "data.nextSteps.title", "data.nextSteps.description", ]; const findProperty: SearchFacets = [ "data.newAddressTitle", - "data.newAddressDescription", + richTextField("data.newAddressDescription"), "data.newAddressDescriptionLabel", ]; const drawBoundary: SearchFacets = [ "data.titleForUploading", - "data.descriptionForUploading", + richTextField("data.descriptionForUploading"), ]; -const planningConstraints: SearchFacets = ["data.disclaimer"]; +const planningConstraints: SearchFacets = [richTextField("data.disclaimer")]; const pay: SearchFacets = [ "data.bannerTitle", "data.instructionsTitle", - "data.instructionsDescription", + richTextField("data.instructionsDescription"), "data.secondaryPageTitle", "data.nomineeTitle", - "data.nomineeDescription", + richTextField("data.nomineeDescription"), "data.yourDetailsTitle", - "data.yourDetailsDescription", + richTextField("data.yourDetailsDescription"), "data.yourDetailsLabel", "data.govPayMetadata.key", "data.govPayMetadata.value", From 405ad3e90e12698d6e231ad8006918a919d0e174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 8 Oct 2024 16:39:53 +0100 Subject: [PATCH 2/6] test: Update existing mocks to match new type --- .../components/Sidebar/Search/mocks/dataFacetFlow.ts | 8 ++++++++ .../FlowEditor/components/Sidebar/Search/mocks/simple.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts index 44f4efeb23..8d11d113d7 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts @@ -208,6 +208,7 @@ export const mockQuestionResult: SearchResult = { key: "data.fn", matchIndices: [[0, 3]], refIndex: 0, + matchValue: "colour", }; export const mockAnswerResult: SearchResult = { @@ -223,6 +224,7 @@ export const mockAnswerResult: SearchResult = { key: "data.val", matchIndices: [[0, 2]], refIndex: 0, + matchValue: "red", }; export const mockListRootResult: SearchResult = { @@ -326,6 +328,7 @@ export const mockListRootResult: SearchResult = { key: "data.fn", matchIndices: [[0, 7]], refIndex: 0, + matchValue: "listRoot", }; export const mockListDataResult: SearchResult = { @@ -467,6 +470,7 @@ export const mockListDataResult: SearchResult = { key: "data.schema.fields.data.fn", matchIndices: [[0, 5]], refIndex: 1, + matchValue: "tenure", }; export const mockListAnswerResult: SearchResult = { @@ -608,6 +612,7 @@ export const mockListAnswerResult: SearchResult = { key: "data.schema.fields.data.options.data.val", matchIndices: [[0, 14]], refIndex: 10, + matchValue: "tenure", }; export const mockCalculateRootResult: SearchResult = { @@ -630,6 +635,7 @@ export const mockCalculateRootResult: SearchResult = { key: "data.output", matchIndices: [[0, 14]], refIndex: 0, + matchValue: "calculateOutput", }; export const mockCalculateFormulaResult: SearchResult = { @@ -652,6 +658,7 @@ export const mockCalculateFormulaResult: SearchResult = { key: "formula", matchIndices: [[0, 6]], refIndex: 1, + matchValue: "formulaTwo", }; export const mockFileUploadAndLabelResult: SearchResult = { @@ -676,4 +683,5 @@ export const mockFileUploadAndLabelResult: SearchResult = { key: "data.fileTypes.fn", matchIndices: [[0, 8]], refIndex: 0, + matchValue: "floorplan", }; 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 391b4bec02..47af0f2526 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 @@ -50,6 +50,7 @@ export const results: SearchResults = [ key: "data.val", matchIndices: [[0, 2]], refIndex: 0, + matchValue: "india", }, { item: { @@ -64,5 +65,6 @@ export const results: SearchResults = [ key: "data.val", matchIndices: [[0, 2]], refIndex: 0, + matchValue: "indonesia", }, ]; From d80c5f8a923d6f04561c4513011cdd8e26bb880e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 8 Oct 2024 17:53:19 +0100 Subject: [PATCH 3/6] fix: Search full strings --- editor.planx.uk/src/hooks/useSearch.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editor.planx.uk/src/hooks/useSearch.ts b/editor.planx.uk/src/hooks/useSearch.ts index a33f4e764c..e8a6f641fb 100644 --- a/editor.planx.uk/src/hooks/useSearch.ts +++ b/editor.planx.uk/src/hooks/useSearch.ts @@ -33,6 +33,7 @@ export const useSearch = ({ useExtendedSearch: true, includeMatches: true, minMatchCharLength: 3, + ignoreLocation: true, keys, }), [keys], From 9298ae6b2188f6689c3673ebb7c9bbb9f5db3268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Mon, 21 Oct 2024 15:05:23 +0100 Subject: [PATCH 4/6] feat: Drop now redundant getHeadline(), update tests --- .../Search/SearchResultCard/allFacets.test.ts | 69 ++++++++++++++----- .../getDisplayDetailsForResult.tsx | 66 ------------------ .../Sidebar/Search/mocks/allFacetFlow.ts | 15 ++++ .../Sidebar/Search/mocks/dataFacetFlow.ts | 2 +- 4 files changed, 68 insertions(+), 84 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts index 0eb50c75be..7362cf47b4 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts @@ -43,6 +43,7 @@ describe("Basic fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.description", + matchValue: "Peacock", }); expect(output).toStrictEqual({ @@ -50,7 +51,7 @@ describe("Basic fields", () => { iconKey: ComponentType.Question, componentType: "Question", title: "Seahorse", - headline: "

Peacock

", + headline: "Peacock", }); }); @@ -72,6 +73,7 @@ describe("More information fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.notes", + matchValue: "Echidna", }); expect(output).toStrictEqual({ @@ -87,6 +89,7 @@ describe("More information fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.howMeasured", + matchValue: "Gazelle", }); expect(output).toStrictEqual({ @@ -94,7 +97,7 @@ describe("More information fields", () => { iconKey: ComponentType.Question, componentType: "Question", title: "Seahorse", - headline: "

Gazelle

", + headline: "Gazelle", }); }); @@ -102,6 +105,7 @@ describe("More information fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.policyRef", + matchValue: "Rat" }); expect(output).toStrictEqual({ @@ -109,7 +113,7 @@ describe("More information fields", () => { iconKey: ComponentType.Question, componentType: "Question", title: "Seahorse", - headline: "

Rat

", + headline: "Rat", }); }); @@ -117,6 +121,7 @@ describe("More information fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.info", + matchValue: "Octopus" }); expect(output).toStrictEqual({ @@ -124,7 +129,7 @@ describe("More information fields", () => { iconKey: ComponentType.Question, componentType: "Question", title: "Seahorse", - headline: "

Octopus

", + headline: "Octopus", }); }); }); @@ -171,6 +176,7 @@ describe("nextSteps fields", () => { const output = getDisplayDetailsForResult({ ...mockNextStepsOptionResult, key: "data.steps.description", + matchValue: "Vulture" }); expect(output).toStrictEqual({ @@ -186,6 +192,7 @@ describe("nextSteps fields", () => { const output = getDisplayDetailsForResult({ ...mockNextStepsOptionResult, key: "data.steps.url", + matchValue: "https://www.starfish.gov.uk", }); expect(output).toStrictEqual({ @@ -215,6 +222,7 @@ describe("fileUploadAndLabel fields", () => { const output = getDisplayDetailsForResult({ ...mockFileUploadAndLabelResult, key: "data.fileTypes.moreInformation.info", + matchValue: "Kangaroo" }); expect(output).toStrictEqual({ @@ -222,7 +230,7 @@ describe("fileUploadAndLabel fields", () => { iconKey: ComponentType.FileUploadAndLabel, componentType: "File upload and label", title: ".", - headline: "

Kangaroo

", + headline: "Kangaroo", }); }); @@ -230,6 +238,7 @@ describe("fileUploadAndLabel fields", () => { const output = getDisplayDetailsForResult({ ...mockFileUploadAndLabelResult, key: "data.fileTypes.moreInformation.policyRef", + matchValue: "Tiger" }); expect(output).toStrictEqual({ @@ -237,7 +246,7 @@ describe("fileUploadAndLabel fields", () => { iconKey: ComponentType.FileUploadAndLabel, componentType: "File upload and label", title: ".", - headline: "

Tiger

", + headline: "Tiger", }); }); @@ -245,6 +254,7 @@ describe("fileUploadAndLabel fields", () => { const output = getDisplayDetailsForResult({ ...mockFileUploadAndLabelResult, key: "data.fileTypes.moreInformation.howMeasured", + matchValue: "Salamander" }); expect(output).toStrictEqual({ @@ -252,7 +262,7 @@ describe("fileUploadAndLabel fields", () => { iconKey: ComponentType.FileUploadAndLabel, componentType: "File upload and label", title: ".", - headline: "

Salamander

", + headline: "Salamander", }); }); }); @@ -287,6 +297,7 @@ describe("schemaComponents fields", () => { const output = getDisplayDetailsForResult({ ...mockSchemaResult, key: "data.schema.fields.data.title", + matchValue: "Donkey", }); expect(output).toStrictEqual({ @@ -302,6 +313,7 @@ describe("schemaComponents fields", () => { const output = getDisplayDetailsForResult({ ...mockSchemaResult, key: "data.schema.fields.data.description", + matchValue: "Alpaca", }); expect(output).toStrictEqual({ @@ -317,6 +329,7 @@ describe("schemaComponents fields", () => { const output = getDisplayDetailsForResult({ ...mockSchemaResult, key: "data.schema.fields.data.options.text", + matchValue: "Iguana", }); expect(output).toStrictEqual({ @@ -332,6 +345,7 @@ describe("schemaComponents fields", () => { const output = getDisplayDetailsForResult({ ...mockSchemaResult, key: "data.schema.fields.data.options.data.description", + matchValue: "Parrot" }); expect(output).toStrictEqual({ @@ -361,6 +375,7 @@ describe("taskList fields", () => { const output = getDisplayDetailsForResult({ ...mockTaskListResult, key: "data.tasks.description", + matchValue: "Beaver", }); expect(output).toStrictEqual({ @@ -368,7 +383,7 @@ describe("taskList fields", () => { iconKey: ComponentType.TaskList, componentType: "Task list", title: ".", - headline: "

Beaver

", + headline: "Beaver", }); }); }); @@ -382,7 +397,7 @@ describe("content fields", () => { iconKey: ComponentType.Content, componentType: "Content", title: "", - headline: "

Sheep

", + headline: "Sheep", }); }); }); @@ -404,6 +419,7 @@ describe("confirmation fields", () => { const output = getDisplayDetailsForResult({ ...mockConfirmationResult, key: "data.moreInfo", + matchValue: "Tarantula", }); expect(output).toStrictEqual({ @@ -411,7 +427,7 @@ describe("confirmation fields", () => { iconKey: ComponentType.Confirmation, componentType: "Confirmation", title: "", - headline: "

Tarantula

", + headline: "Tarantula", }); }); @@ -419,6 +435,7 @@ describe("confirmation fields", () => { const output = getDisplayDetailsForResult({ ...mockConfirmationResult, key: "data.contactInfo", + matchValue: "Weasel" }); expect(output).toStrictEqual({ @@ -426,7 +443,7 @@ describe("confirmation fields", () => { iconKey: ComponentType.Confirmation, componentType: "Confirmation", title: "", - headline: "

Weasel

", + headline: "Weasel", }); }); @@ -434,6 +451,7 @@ describe("confirmation fields", () => { const output = getDisplayDetailsForResult({ ...mockConfirmationResult, key: "data.nextSteps.title", + matchValue: "Llama" }); expect(output).toStrictEqual({ @@ -449,6 +467,7 @@ describe("confirmation fields", () => { const output = getDisplayDetailsForResult({ ...mockConfirmationResult, key: "data.nextSteps.description", + matchValue: "Toucan", }); expect(output).toStrictEqual({ @@ -478,6 +497,7 @@ describe("findProperty fields", () => { const output = getDisplayDetailsForResult({ ...mockFindPropertyResult, key: "data.newAddressDescription", + matchValue: "Stingray", }); expect(output).toStrictEqual({ @@ -485,7 +505,7 @@ describe("findProperty fields", () => { iconKey: ComponentType.FindProperty, componentType: "Find property", title: ".", - headline: "

Stingray

", + headline: "Stingray", }); }); @@ -493,6 +513,7 @@ describe("findProperty fields", () => { const output = getDisplayDetailsForResult({ ...mockFindPropertyResult, key: "data.newAddressDescriptionLabel", + matchValue: "Scorpion", }); expect(output).toStrictEqual({ @@ -522,6 +543,7 @@ describe("drawBoundary fields", () => { const output = getDisplayDetailsForResult({ ...mockDrawBoundaryResult, key: "data.descriptionForUploading", + matchValue: "Panda" }); expect(output).toStrictEqual({ @@ -529,7 +551,7 @@ describe("drawBoundary fields", () => { iconKey: ComponentType.DrawBoundary, componentType: "Draw boundary", title: ".", - headline: "

Panda

", + headline: "Panda", }); }); }); @@ -543,7 +565,7 @@ describe("planningConstraints fields", () => { iconKey: ComponentType.PlanningConstraints, componentType: "Planning constraints", title: ".", - headline: "

Barracuda

", + headline: "Barracuda", }); }); }); @@ -553,6 +575,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.bannerTitle", + matchValue: "Moose", }); expect(output).toStrictEqual({ @@ -568,6 +591,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.instructionsTitle", + matchValue: "Pelican", }); expect(output).toStrictEqual({ @@ -583,6 +607,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.instructionsDescription", + matchValue: "Cockatoo", }); expect(output).toStrictEqual({ @@ -590,7 +615,7 @@ describe("pay fields", () => { iconKey: ComponentType.Pay, componentType: "Pay", title: "Jaguar", - headline: "

Cockatoo

", + headline: "Cockatoo", }); }); @@ -598,6 +623,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.secondaryPageTitle", + matchValue: "Chicken", }); expect(output).toStrictEqual({ @@ -613,6 +639,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.nomineeTitle", + matchValue: "Aardvark", }); expect(output).toStrictEqual({ @@ -628,6 +655,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.nomineeDescription", + matchValue: "Cheetah", }); expect(output).toStrictEqual({ @@ -635,7 +663,7 @@ describe("pay fields", () => { iconKey: ComponentType.Pay, componentType: "Pay", title: "Jaguar", - headline: "

Cheetah

", + headline: "Cheetah", }); }); @@ -643,6 +671,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.yourDetailsTitle", + matchValue: "Camel", }); expect(output).toStrictEqual({ @@ -658,6 +687,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.yourDetailsDescription", + matchValue: "Macaw" }); expect(output).toStrictEqual({ @@ -665,7 +695,7 @@ describe("pay fields", () => { iconKey: ComponentType.Pay, componentType: "Pay", title: "Jaguar", - headline: "

Macaw

", + headline: "Macaw", }); }); @@ -673,6 +703,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.yourDetailsLabel", + matchValue: "Skunk", }); expect(output).toStrictEqual({ @@ -688,6 +719,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.govPayMetadata.key", + matchValue: "Tapir", }); expect(output).toStrictEqual({ @@ -703,6 +735,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.govPayMetadata.value", + matchValue: "Okapi", }); expect(output).toStrictEqual({ @@ -721,6 +754,7 @@ describe("result fields", () => { const output = getDisplayDetailsForResult({ ...mockResultResult, key: "data.overrides.IMMUNE.heading", + matchValue: "Squid" }); expect(output).toStrictEqual({ @@ -736,6 +770,7 @@ describe("result fields", () => { const output = getDisplayDetailsForResult({ ...mockResultResult, key: "data.overrides.IMMUNE.description", + matchValue: "Eagle", }); expect(output).toStrictEqual({ diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx index 86dfa158c5..a19e7cd7e5 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx @@ -43,8 +43,6 @@ const keyFormatters: KeyMap = { }, "data.fileTypes.fn": { getDisplayKey: () => "File type (data)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as FileUploadAndLabel).fileTypes[refIndex].fn!, }, "data.dataFieldBoundary": { getDisplayKey: () => "Boundary", @@ -71,46 +69,27 @@ const keyFormatters: KeyMap = { getDisplayKey: () => "Why it matters", }, "data.categories.title": { - getHeadline: ({ item, refIndex }) => - (item.data as unknown as Checklist).categories![refIndex].title, }, "data.steps.title": { getDisplayKey: () => "Title (step)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as NextSteps).steps[refIndex].title, }, "data.steps.description": { getDisplayKey: () => "Description (step)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as NextSteps).steps[refIndex].description, }, "data.steps.url": { getDisplayKey: () => "URL (step)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as NextSteps).steps[refIndex].url!, }, "data.fileTypes.name": { getDisplayKey: () => "Name (file type)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as FileUploadAndLabel).fileTypes[refIndex].name, }, "data.fileTypes.moreInformation.howMeasured": { getDisplayKey: () => "How is it defined (file type)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as FileUploadAndLabel).fileTypes[refIndex] - .moreInformation!.howMeasured!, }, "data.fileTypes.moreInformation.policyRef": { getDisplayKey: () => "Policy reference (file type)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as FileUploadAndLabel).fileTypes[refIndex] - .moreInformation!.policyRef!, }, "data.fileTypes.moreInformation.info": { getDisplayKey: () => "Why it matters (file type)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as FileUploadAndLabel).fileTypes[refIndex] - .moreInformation!.info!, }, "data.units": { getDisplayKey: () => "Unit type", @@ -118,47 +97,20 @@ const keyFormatters: KeyMap = { "data.schemaName": { getDisplayKey: () => "Schema name", }, - "data.schema.fields.data.title": { - getHeadline: ({ item, refIndex }) => - (item.data?.schema as unknown as Schema).fields[refIndex].data.title, - }, "data.schema.fields.data.description": { getDisplayKey: () => "Description", - getHeadline: ({ item, refIndex }) => - (item.data?.schema as unknown as Schema).fields[refIndex].data - .description!, }, "data.schema.fields.data.options.data.description": { getDisplayKey: () => "Option (description)", - getHeadline: ({ item, refIndex }) => { - const options = (item.data?.schema as unknown as Schema).fields - .filter( - (field) => field.type === "question" || field.type === "checklist", - ) - .flatMap((field) => field.data.options); - return options[refIndex].data.description || ""; - }, }, "data.schema.fields.data.options.text": { getDisplayKey: () => "Option", - getHeadline: ({ item, refIndex }) => { - const options = (item.data?.schema as unknown as Schema).fields - .filter( - (field) => field.type === "question" || field.type === "checklist", - ) - .flatMap((field) => field.data.options); - return options[refIndex].data.text || ""; - }, }, "data.tasks.title": { getDisplayKey: () => "Title (task)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as TaskList).tasks[refIndex].title, }, "data.tasks.description": { getDisplayKey: () => "Description (task)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as TaskList).tasks[refIndex].description, }, ...Object.fromEntries( flatFlags.flatMap(({ value }) => [ @@ -187,13 +139,9 @@ const keyFormatters: KeyMap = { }, "data.nextSteps.title": { getDisplayKey: () => "Title (next steps)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as Confirmation).nextSteps![refIndex].title, }, "data.nextSteps.description": { getDisplayKey: () => "Description (next steps)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as Confirmation).nextSteps![refIndex].description, }, "data.newAddressTitle": { getDisplayKey: () => "Title (new address)", @@ -242,13 +190,9 @@ const keyFormatters: KeyMap = { }, "data.govPayMetadata.key": { getDisplayKey: () => "GOV.UK Pay metadata (key)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as Pay).govPayMetadata[refIndex].key, }, "data.govPayMetadata.value": { getDisplayKey: () => "GOV.UK Pay metadata (value)", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as Pay).govPayMetadata[refIndex].value.toString(), }, // Calculate contains both input and output data values formula: { @@ -257,23 +201,13 @@ const keyFormatters: KeyMap = { }, "data.output": { getDisplayKey: () => "Output (data)", - getHeadline: ({ item }) => (item.data as unknown as Calculate).output, }, // List contains data variables nested within its schema "data.schema.fields.data.fn": { getDisplayKey: () => "Data", - getHeadline: ({ item, refIndex }) => - (item.data as unknown as List).schema.fields[refIndex].data.fn, }, "data.schema.fields.data.options.data.val": { getDisplayKey: () => "Option (data)", - 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") - .flatMap((field) => field.data.options); - return options[refIndex].data.val || ""; - }, }, }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/allFacetFlow.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/allFacetFlow.ts index e82e27da5b..682cc4c642 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/allFacetFlow.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/allFacetFlow.ts @@ -295,6 +295,7 @@ export const mockQuestionResult: SearchResult = { key: "data.text", matchIndices: [[0, 7]], refIndex: 0, + matchValue: "Seahorse", }; export const mockPayResult: SearchResult = { @@ -340,6 +341,7 @@ export const mockPayResult: SearchResult = { key: "data.title", matchIndices: [[0, 5]], refIndex: 3, + matchValue: "Jaguar", }; export const mockChecklistResult: SearchResult = { @@ -363,6 +365,7 @@ export const mockChecklistResult: SearchResult = { key: "data.categories.title", matchIndices: [[0, 4]], refIndex: 0, + matchValue: "Koala", }; export const mockChecklistOptionResult: SearchResult = { @@ -378,6 +381,7 @@ export const mockChecklistOptionResult: SearchResult = { key: "data.text", matchIndices: [[0, 3]], refIndex: 0, + matchValue: "Duck", }; export const mockNextStepsOptionResult: SearchResult = { @@ -400,6 +404,7 @@ export const mockNextStepsOptionResult: SearchResult = { key: "data.steps.title", matchIndices: [[0, 6]], refIndex: 0, + matchValue: "Hamster", }; export const mockFileUploadAndLabelResult: SearchResult = { @@ -429,6 +434,7 @@ export const mockFileUploadAndLabelResult: SearchResult = { key: "data.fileTypes.name", matchIndices: [[0, 6]], refIndex: 0, + matchValue: "Penguin", }; export const mockNumberInputResult: SearchResult = { @@ -444,6 +450,7 @@ export const mockNumberInputResult: SearchResult = { key: "data.units", matchIndices: [[0, 8]], refIndex: 0, + matchValue: "Wolverine", }; export const mockSchemaResult: SearchResult = { @@ -497,6 +504,7 @@ export const mockSchemaResult: SearchResult = { key: "data.schemaName", matchIndices: [[0, 7]], refIndex: 0, + matchValue: "Hedgehog", }; export const mockTaskListResult: SearchResult = { @@ -518,6 +526,7 @@ export const mockTaskListResult: SearchResult = { key: "data.tasks.title", matchIndices: [[0, 6]], refIndex: 0, + matchValue: "Ostrich", }; export const mockContentResult: SearchResult = { @@ -532,6 +541,7 @@ export const mockContentResult: SearchResult = { key: "data.content", matchIndices: [[3, 7]], refIndex: 0, + matchValue: "Sheep", }; export const mockConfirmationResult: SearchResult = { @@ -558,6 +568,7 @@ export const mockConfirmationResult: SearchResult = { key: "data.heading", matchIndices: [[0, 4]], refIndex: 0, + matchValue: "Snake", }; export const mockFindPropertyResult: SearchResult = { @@ -576,6 +587,7 @@ export const mockFindPropertyResult: SearchResult = { key: "data.newAddressTitle", matchIndices: [[0, 4]], refIndex: 0, + matchValue: "Mouse", }; export const mockDrawBoundaryResult: SearchResult = { @@ -596,6 +608,7 @@ export const mockDrawBoundaryResult: SearchResult = { key: "data.titleForUploading", matchIndices: [[0, 7]], refIndex: 0, + matchValue: "Elephant", }; export const mockPlanningConstraintsResult: SearchResult = { @@ -613,6 +626,7 @@ export const mockPlanningConstraintsResult: SearchResult = { key: "data.disclaimer", matchIndices: [[3, 11]], refIndex: 0, + matchValue: "Barracuda", }; export const mockResultResult: SearchResult = { @@ -633,4 +647,5 @@ export const mockResultResult: SearchResult = { key: "data.overrides.IMMUNE.heading", matchIndices: [[0, 4]], refIndex: 0, + matchValue: "Squid", }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts index 8d11d113d7..f204dd44fb 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/dataFacetFlow.ts @@ -612,7 +612,7 @@ export const mockListAnswerResult: SearchResult = { key: "data.schema.fields.data.options.data.val", matchIndices: [[0, 14]], refIndex: 10, - matchValue: "tenure", + matchValue: "selfCustomBuild", }; export const mockCalculateRootResult: SearchResult = { From 12dfdb1e6909dd10ea49e9df4803a8ed0b902e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 22 Oct 2024 13:57:28 +0100 Subject: [PATCH 5/6] feat: Handle newlines --- .../components/Sidebar/Search/Headline.tsx | 1 + .../FlowEditor/components/Sidebar/Search/facets.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) 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 index f4699110b1..1c181dc819 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.tsx @@ -19,6 +19,7 @@ export const Headline: React.FC = ({ text, matchIndices, variant }) => { ({ fontWeight: isHighlighted(index) ? FONT_WEIGHT_BOLD : "regular", fontSize: theme.typography.body2.fontSize, diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts index 0a163b40bc..3d053ad539 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/facets.ts @@ -2,8 +2,6 @@ import { flatFlags, IndexedNode } from "@opensystemslab/planx-core/types"; import { FuseOptionKey, FuseOptionKeyObject } from "fuse.js"; import { get } from "lodash"; -import { stripTagsAndLimitLength } from "../../Flow/lib/utils"; - export type SearchFacets = Array>; const generalData: SearchFacets = ["data.fn", "data.val"]; @@ -37,6 +35,14 @@ export const DATA_FACETS: SearchFacets = [ ...drawBoundaryData, ]; +const stripHTMLTags = (html = "") => + html + // Replace HTML tags with newlines + .replace(/<[^>]+>/g, "\n") + // Collapse multiple newlines + .replace(/\n\s*\n/g, "\n") + .trim(); + /** * Generate a Fuse getFn in order to search against the text content of the HTML generated by RichTextInput fields * Strips HTML tags from searched value in order to maintain matchIndices @@ -47,7 +53,7 @@ const richTextField = ( ): FuseOptionKeyObject => ({ name: key, getFn: (node: IndexedNode) => - stripTagsAndLimitLength(get(node as Record, key) || "", ""), + stripHTMLTags(get(node as Record, key)), }); const basicFields: SearchFacets = [ From f0cf4fd64c5fd9e1bbc4713f53d75f642e3e526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 23 Oct 2024 11:01:20 +0100 Subject: [PATCH 6/6] test: Add strip html test --- .../Search/SearchResultCard/allFacets.test.ts | 26 +++++++------- .../components/Sidebar/Search/index.test.tsx | 35 +++++++++++++++++++ .../components/Sidebar/Search/mocks/simple.ts | 2 ++ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts index 7362cf47b4..c4089230e9 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts @@ -105,7 +105,7 @@ describe("More information fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.policyRef", - matchValue: "Rat" + matchValue: "Rat", }); expect(output).toStrictEqual({ @@ -121,7 +121,7 @@ describe("More information fields", () => { const output = getDisplayDetailsForResult({ ...mockQuestionResult, key: "data.info", - matchValue: "Octopus" + matchValue: "Octopus", }); expect(output).toStrictEqual({ @@ -133,6 +133,7 @@ describe("More information fields", () => { }); }); }); + describe("checklist fields", () => { it("renders data.categories.title", () => { const output = getDisplayDetailsForResult(mockChecklistResult); @@ -176,7 +177,7 @@ describe("nextSteps fields", () => { const output = getDisplayDetailsForResult({ ...mockNextStepsOptionResult, key: "data.steps.description", - matchValue: "Vulture" + matchValue: "Vulture", }); expect(output).toStrictEqual({ @@ -222,7 +223,7 @@ describe("fileUploadAndLabel fields", () => { const output = getDisplayDetailsForResult({ ...mockFileUploadAndLabelResult, key: "data.fileTypes.moreInformation.info", - matchValue: "Kangaroo" + matchValue: "Kangaroo", }); expect(output).toStrictEqual({ @@ -238,7 +239,7 @@ describe("fileUploadAndLabel fields", () => { const output = getDisplayDetailsForResult({ ...mockFileUploadAndLabelResult, key: "data.fileTypes.moreInformation.policyRef", - matchValue: "Tiger" + matchValue: "Tiger", }); expect(output).toStrictEqual({ @@ -254,7 +255,7 @@ describe("fileUploadAndLabel fields", () => { const output = getDisplayDetailsForResult({ ...mockFileUploadAndLabelResult, key: "data.fileTypes.moreInformation.howMeasured", - matchValue: "Salamander" + matchValue: "Salamander", }); expect(output).toStrictEqual({ @@ -280,6 +281,7 @@ describe("numberInput fields", () => { }); }); }); + describe("schemaComponents fields", () => { it("renders data.schemaName", () => { const output = getDisplayDetailsForResult(mockSchemaResult); @@ -345,7 +347,7 @@ describe("schemaComponents fields", () => { const output = getDisplayDetailsForResult({ ...mockSchemaResult, key: "data.schema.fields.data.options.data.description", - matchValue: "Parrot" + matchValue: "Parrot", }); expect(output).toStrictEqual({ @@ -435,7 +437,7 @@ describe("confirmation fields", () => { const output = getDisplayDetailsForResult({ ...mockConfirmationResult, key: "data.contactInfo", - matchValue: "Weasel" + matchValue: "Weasel", }); expect(output).toStrictEqual({ @@ -451,7 +453,7 @@ describe("confirmation fields", () => { const output = getDisplayDetailsForResult({ ...mockConfirmationResult, key: "data.nextSteps.title", - matchValue: "Llama" + matchValue: "Llama", }); expect(output).toStrictEqual({ @@ -543,7 +545,7 @@ describe("drawBoundary fields", () => { const output = getDisplayDetailsForResult({ ...mockDrawBoundaryResult, key: "data.descriptionForUploading", - matchValue: "Panda" + matchValue: "Panda", }); expect(output).toStrictEqual({ @@ -687,7 +689,7 @@ describe("pay fields", () => { const output = getDisplayDetailsForResult({ ...mockPayResult, key: "data.yourDetailsDescription", - matchValue: "Macaw" + matchValue: "Macaw", }); expect(output).toStrictEqual({ @@ -754,7 +756,7 @@ describe("result fields", () => { const output = getDisplayDetailsForResult({ ...mockResultResult, key: "data.overrides.IMMUNE.heading", - matchValue: "Squid" + matchValue: "Squid", }); expect(output).toStrictEqual({ diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx index 6501ae59b1..76e2b3635e 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx @@ -122,3 +122,38 @@ it("should not have any accessibility violations on initial load", async () => { const results = await axe(container); expect(results).toHaveNoViolations(); }); + +describe("rich text fields", () => { + test("HTML tags are stripped out", async () => { + const { + user, + getByRole, + getAllByRole, + getByText, + queryByText, + getByLabelText, + } = setup( + + + , + ); + + const searchInput = getByLabelText("Search this flow and internal portals"); + user.type(searchInput, "rich text"); + + // Search has completed + await waitFor(() => expect(getByRole("list")).toBeInTheDocument()); + await waitFor(() => expect(getAllByRole("listitem")).toHaveLength(1)); + + // Single, correct, search result returned which has rich text as a description + expect(getByText(/1 result:/)).toBeVisible(); + expect(getByText(/Pick a country/)).toBeVisible(); + expect(getByText(/Description/)).toBeVisible(); + + // No HTML tags in text + // We must search by characters and not strings (e.g ) as the string is split for the headline + expect(queryByText(//)).not.toBeInTheDocument(); + expect(queryByText(/\//)).not.toBeInTheDocument(); + }); +}); 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 47af0f2526..4f5943599c 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 @@ -10,6 +10,8 @@ export const flow: FlowGraph = { data: { fn: "country", text: "Pick a country", + description: + "

This is rich text

  1. With many nested html tags
  2. Like thisone
    1. ", }, edges: ["VhSydY2fTe", "tR9tdaWOvF", "tvUxd2IoPo"], },