From e03421c664a9d77a02c79664a568585c96e2429e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 08:25:31 +0000 Subject: [PATCH 01/11] docs: How to add fields to search (#4016) --- doc/how-to/how-to-add-fields-to-search.md | 90 +++++++++++++++++++ .../docs/adding-a-new-component.md | 4 + 2 files changed, 94 insertions(+) create mode 100644 doc/how-to/how-to-add-fields-to-search.md diff --git a/doc/how-to/how-to-add-fields-to-search.md b/doc/how-to/how-to-add-fields-to-search.md new file mode 100644 index 0000000000..fb6c12062f --- /dev/null +++ b/doc/how-to/how-to-add-fields-to-search.md @@ -0,0 +1,90 @@ +# How to add fields to the Editor search index + +## Overview + +This guide outlines the process of adding new searchable fields to the PlanX Editors' frontend search functionality, which uses Fuse.js for indexing and searching. This is a required step with adding a new component to PlanX, or adding new fields to an existing component. + +## Background + +- Search is currently implemented in the frontend using Fuse.js +- Only certain fields are searchable: + - Text fields (simple text) + - Rich text fields (HTML) + - Data values (e.g. `data.fn`) + +## Process + +### 1. Update facets configuration + +Location: `src/pages/FlowEditor/components/Sidebar/Search/facets.ts` + +#### Guidelines: +- Use key paths to the new fields (e.g. `data.myNewField`) +- Wrap rich text fields with `richTextField()` helper - this strips HTML tags +- Add data fields to `DATA_FACETS` +- Add text fields to `ALL_FACETS` +- Avoid adding duplicate values already held in `ALL_FACETS` (e.g., `data.title`, `data.description`) + +#### Example: + +```ts +// facets.ts + +const myNewComponent: SearchFacets = [ + richTextField("data.myRichTextField"), + "data.myPlainTextField" +]; + +export const ALL_FACETS: SearchFacets = [ + ...otherComponents, + ...myNewComponent, + ...DATA_FACETS, +]; +``` + +### 2. Assign display values + +Location: `src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/getDisplayDetailsForResult.tsx` + +#### Add key formatters: + +```ts +// getDisplayDetailsForResult.tsx + +const keyFormatters: KeyMap = { + ...existingFormatters, + "data.myNewField": { + getDisplayKey: () => "My New Field", + }, +}; +``` + +### 3. Update tests + +Locations: +- `src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/allFacets.test.ts` +- `src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/dataFacets.test.ts` + +#### Test steps: +1. Add new component to `mockFlow` +2. Create mock search result + - Example: `mockMyComponentResult: SearchResult` + +### Debugging tip + +A search result can be easily logged to the console from `SearchResultCard`. Simply search for one of your new fields, and click on the card. + +```ts +// SearchResultCard/index.tsx + +const handleClick = () => { + const url = getURLForNode(result.item.id); + // Temporarily disable navigation + // navigate(url); + + // Log the full search result to console + console.log({ result }); +}; +``` + +For reference, [please see this PR](https://github.com/theopensystemslab/planx-new/pull/4015) which added the text fields for the `Feedback` component to the search index. \ No newline at end of file diff --git a/editor.planx.uk/docs/adding-a-new-component.md b/editor.planx.uk/docs/adding-a-new-component.md index d04fa57a10..121a03930c 100644 --- a/editor.planx.uk/docs/adding-a-new-component.md +++ b/editor.planx.uk/docs/adding-a-new-component.md @@ -122,6 +122,10 @@ import SetValue from "@planx/components/SetValue/Editor"; set: SetValueComponent, ``` +6. Add text fields to search index + +Please see detailed guide here - https://github.com/theopensystemslab/planx-new/blob/main/doc/how-to/how-to-add-fields-to-search.md + ## Preview environment & integrations 1. `src/pages/Preview/Node.tsx` From bce9028e5f2b3452c8d3d8cc9ab3886872ceb147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 08:25:48 +0000 Subject: [PATCH 02/11] feat(search): Add new `Feedback` component fields to search index (#4015) --- .../Search/SearchResultCard/allFacets.test.ts | 47 +++++++++++++++++++ .../getDisplayDetailsForResult.tsx | 6 +++ .../components/Sidebar/Search/facets.ts | 7 +++ .../Sidebar/Search/mocks/allFacetFlow.ts | 32 +++++++++++++ 4 files changed, 92 insertions(+) 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 c4089230e9..6f016a147e 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 @@ -7,6 +7,7 @@ import { mockConfirmationResult, mockContentResult, mockDrawBoundaryResult, + mockFeedbackResult, mockFileUploadAndLabelResult, mockFindPropertyResult, mockFlow, @@ -784,3 +785,49 @@ describe("result fields", () => { }); }); }); + +describe("feedback fields", () => { + it("renders data.ratingQuestion", () => { + const output = getDisplayDetailsForResult(mockFeedbackResult); + + expect(output).toStrictEqual({ + key: "Rating question", + iconKey: ComponentType.Feedback, + componentType: "Feedback", + title: "title text", + headline: "Bullfrog", + }); + }); + + it("renders data.freeformQuestion", () => { + const output = getDisplayDetailsForResult({ + ...mockFeedbackResult, + key: "data.freeformQuestion", + matchValue: "Oarfish", + }); + + expect(output).toStrictEqual({ + key: "Freeform question", + iconKey: ComponentType.Feedback, + componentType: "Feedback", + title: "title text", + headline: "Oarfish", + }); + }); + + it("renders data.disclaimer", () => { + const output = getDisplayDetailsForResult({ + ...mockFeedbackResult, + key: "data.disclaimer", + matchValue: "Wagtail", + }); + + expect(output).toStrictEqual({ + key: "Disclaimer", + iconKey: ComponentType.Feedback, + componentType: "Feedback", + title: "title text", + headline: "Wagtail", + }); + }); +}); 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 1ec69a97fd..c725d96214 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 @@ -208,6 +208,12 @@ const keyFormatters: KeyMap = { "data.schema.fields.data.options.data.val": { getDisplayKey: () => "Option (data)", }, + "data.ratingQuestion": { + getDisplayKey: () => "Rating question", + }, + "data.freeformQuestion": { + getDisplayKey: () => "Freeform question", + }, }; const componentFormatters: ComponentMap = { 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 3d053ad539..a85b4329db 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 @@ -150,6 +150,12 @@ const pay: SearchFacets = [ "data.govPayMetadata.value", ]; +const feedback: SearchFacets = [ + richTextField("data.ratingQuestion"), + richTextField("data.freeformQuestion"), + richTextField("data.disclaimer"), +]; + export const ALL_FACETS: SearchFacets = [ ...basicFields, ...moreInformation, @@ -166,5 +172,6 @@ export const ALL_FACETS: SearchFacets = [ ...drawBoundary, ...planningConstraints, ...pay, + ...feedback, ...DATA_FACETS, ]; 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 7ceba417b2..9d3e91b43d 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 @@ -23,6 +23,7 @@ export const mockFlow: FlowGraph = { "9jiJvRYka6", "G6L9c8Sllg", "gyyVEMm9Yb", + "ZVZx9UDPqb", ], }, "6rgnMh94zu": { @@ -274,6 +275,17 @@ export const mockFlow: FlowGraph = { }, type: 9, }, + ZVZx9UDPqb: { + data: { + title: "title text", + disclaimer: "

Oarfish

", + description: "

description text

", + ratingQuestion: "

Bullfrog

", + feedbackRequired: false, + freeformQuestion: "

Wagtail

", + }, + type: 900, + }, }; export const mockQuestionResult: SearchResult = { @@ -649,3 +661,23 @@ export const mockResultResult: SearchResult = { refIndex: 0, matchValue: "Squid", }; + +export const mockFeedbackResult: SearchResult = { + item: { + id: "ZVZx9UDPqb", + parentId: "_root", + type: 900, + data: { + title: "title text", + disclaimer: "

Oarfish

", + description: "

description text

", + ratingQuestion: "

Bullfrog

", + feedbackRequired: false, + freeformQuestion: "

Wagtail

", + }, + }, + key: "data.ratingQuestion", + matchIndices: [[0, 7]], + matchValue: "Bullfrog", + refIndex: 0, +}; From 7a523e9f8165cd88e7863c6b4cd69a6d7deb40fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 08:26:45 +0000 Subject: [PATCH 03/11] fix: Ensure `hasComponentType` checks `fn` value (#4014) --- api.planx.uk/modules/flows/validate/helpers.ts | 10 ++++------ api.planx.uk/modules/flows/validate/validate.test.ts | 12 +++--------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/api.planx.uk/modules/flows/validate/helpers.ts b/api.planx.uk/modules/flows/validate/helpers.ts index 577949bbb2..cbc4c3770e 100644 --- a/api.planx.uk/modules/flows/validate/helpers.ts +++ b/api.planx.uk/modules/flows/validate/helpers.ts @@ -22,14 +22,12 @@ export const hasComponentType = ( const nodeIds = Object.entries(flowGraph).filter( (entry): entry is [string, Node] => isComponentType(entry, type), ); + if (fn) { - nodeIds - ?.filter(([_nodeId, nodeData]) => nodeData?.data?.fn === fn) - ?.map(([nodeId, _nodeData]) => nodeId); - } else { - nodeIds?.map(([nodeId, _nodeData]) => nodeId); + return nodeIds.some(([, nodeData]) => nodeData?.data?.fn === fn); } - return Boolean(nodeIds?.length); + + return Boolean(nodeIds.length); }; export const numberOfComponentType = ( diff --git a/api.planx.uk/modules/flows/validate/validate.test.ts b/api.planx.uk/modules/flows/validate/validate.test.ts index 302b50fd14..46aa4a0833 100644 --- a/api.planx.uk/modules/flows/validate/validate.test.ts +++ b/api.planx.uk/modules/flows/validate/validate.test.ts @@ -419,15 +419,9 @@ describe("invite to pay validation on diff", () => { }); it("does not update if invite to pay is enabled, but there is not a Checklist that sets `proposal.projectType`", async () => { - const { - Checklist: _Checklist, - ChecklistOptionOne: _ChecklistOptionOne, - ChecklistOptionTwo: _ChecklistOptionTwo, - ...invalidatedFlow - } = flowWithInviteToPay; - invalidatedFlow["_root"].edges?.splice( - invalidatedFlow["_root"].edges?.indexOf("Checklist"), - ); + const invalidatedFlow = flowWithInviteToPay; + // Remove proposal.projectType, set incorrect variable + invalidatedFlow!.Checklist!.data!.fn = "some.other.variable"; queryMock.mockQuery({ name: "GetFlowData", From bcf0e475dcdd3eee8d65368c1e6817e9e03c647c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 16:06:45 +0000 Subject: [PATCH 04/11] fix: Clone styling for Questions with data variables or tags (#4017) --- .../components/Flow/components/Question.tsx | 39 ++++++++++--------- .../src/pages/FlowEditor/floweditor.scss | 3 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Question.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Question.tsx index f385aa5072..437c5b33c4 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Question.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Question.tsx @@ -1,4 +1,5 @@ import ErrorIcon from "@mui/icons-material/Error"; +import Box from "@mui/material/Box"; import { ComponentType as TYPES, NodeTags, @@ -76,25 +77,27 @@ const Question: React.FC = React.memo((props) => { }, )} > - - {props.data?.img && ( - + + + {props.data?.img && ( + + )} + {Icon && } + {props.text} + + {props.type !== TYPES.SetValue && props.data?.fn && ( + )} - {Icon && } - {props.text} - - {props.type !== TYPES.SetValue && props.data?.fn && ( - - )} - {props.tags?.map((tag) => )} + {props.tags?.map((tag) => )} +
    {childNodes.map((child: any) => ( diff --git a/editor.planx.uk/src/pages/FlowEditor/floweditor.scss b/editor.planx.uk/src/pages/FlowEditor/floweditor.scss index 510847f098..568dde79a5 100644 --- a/editor.planx.uk/src/pages/FlowEditor/floweditor.scss +++ b/editor.planx.uk/src/pages/FlowEditor/floweditor.scss @@ -119,8 +119,7 @@ $fontMonospace: "Source Code Pro", monospace; outline-offset: 0; } - &.isClone > div, - &.isClone > a { + &.isClone > div { margin-top: 3px; position: relative; ::before { From da116496086b32f99b07c5257fc10f613b1a3de0 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 2 Dec 2024 10:55:03 +0100 Subject: [PATCH 05/11] feat: visually indicate if a node has help text in the graph (#4021) --- .../components/Flow/components/Checklist.tsx | 8 ++++- .../components/Flow/components/Question.tsx | 8 ++++- .../FlowEditor/ToggleHelpTextButton.tsx | 35 +++++++++++++++++++ .../src/pages/FlowEditor/index.tsx | 2 ++ .../src/pages/FlowEditor/lib/store/editor.ts | 7 ++++ .../src/ui/editor/InternalNotes.tsx | 4 +-- .../MoreInformation/MoreInformation.tsx | 4 +-- 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/FlowEditor/ToggleHelpTextButton.tsx diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Checklist.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Checklist.tsx index b56db2325c..eacbadf534 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Checklist.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Flow/components/Checklist.tsx @@ -1,3 +1,4 @@ +import Help from "@mui/icons-material/Help"; import Box from "@mui/material/Box"; import { ComponentType as TYPES, @@ -25,10 +26,11 @@ type Props = { } & NodeTags; const Checklist: React.FC = React.memo((props) => { - const [isClone, childNodes, copyNode] = useStore((state) => [ + const [isClone, childNodes, copyNode, showHelpText] = useStore((state) => [ state.isClone, state.childNodesOf(props.id), state.copyNode, + state.showHelpText, ]); const parent = getParentId(props.parent); @@ -76,6 +78,9 @@ const Checklist: React.FC = React.memo((props) => { const Icon = ICONS[props.type]; + const hasHelpText = + props.data.policyRef || props.data.info || props.data.howMeasured; + return ( <>