Repository: theopensystemslab/planx-new
Pull request #4601: feat: add customisable disclaimer to the Review component
Author: login: augustlindemer
Created: 2025-04-20T16:13:19Z | Merged: 2025-04-24T13:34:43Z

Adds a customisable disclaimer to the Review component. 

First commit adds a rich text input _Disclaimer_ section to the editor modal, with default content.

Second commit adds a somewhat sketchy approach using the standard library method 'find' to (inefficiently) search for the Review component by node type and retrieve its disclaimer. I can't see anything else using this approach, so presumably there are cleaner and more efficient ways of retrieving the data of the Review component.

Unfortunately any of my attempts to retrieve the disclaimer through props defaulted to the default content. I'd much appreciate suggestions!


Diff:
diff --git a/editor.planx.uk/src/@planx/components/Review/Editor.tsx b/editor.planx.uk/src/@planx/components/Review/Editor.tsx
index 7d76b38989..bb215c2fbe 100644
--- a/editor.planx.uk/src/@planx/components/Review/Editor.tsx
+++ b/editor.planx.uk/src/@planx/components/Review/Editor.tsx
@@ -49,6 +49,17 @@ function Component(props: Props) {
             />
           </InputRow>
         </ModalSectionContent>
+        <ModalSectionContent subtitle="Disclaimer">
+          <InputRow>
+            <RichTextInput
+              name="disclaimer"
+              placeholder="Disclaimer"
+              value={formik.values.disclaimer}
+              onChange={formik.handleChange}
+              disabled={props.disabled}
+            />
+          </InputRow>
+        </ModalSectionContent>
       </ModalSection>
       <ModalFooter
         formik={formik}
diff --git a/editor.planx.uk/src/@planx/components/Review/Public/Presentational.tsx b/editor.planx.uk/src/@planx/components/Review/Public/Presentational.tsx
index 636bb8bc91..75e4d8b06b 100644
--- a/editor.planx.uk/src/@planx/components/Review/Public/Presentational.tsx
+++ b/editor.planx.uk/src/@planx/components/Review/Public/Presentational.tsx
@@ -1,7 +1,7 @@
 import { NodeId } from "@opensystemslab/planx-core/types";
 import Card from "@planx/components/shared/Preview/Card";
 import { CardHeader } from "@planx/components/shared/Preview/CardHeader/CardHeader";
-import SummaryListsBySections from "@planx/components/shared/Preview/SummaryList";
+import SummaryLists from "@planx/components/shared/Preview/SummaryList";
 import { PrintButton } from "components/PrintButton";
 import { Store } from "pages/FlowEditor/lib/store";
 import { sortBreadcrumbs } from "pages/FlowEditor/lib/store/preview";
@@ -13,6 +13,7 @@ export default Component;
 interface Props {
   title: string;
   description: string;
+  disclaimer: string;
   breadcrumbs: Store.Breadcrumbs;
   flow: Store.Flow;
   passport: Store.Passport;
@@ -31,13 +32,14 @@ function Component(props: Props) {
   return (
     <Card isValid handleSubmit={props.handleSubmit}>
       <CardHeader title={props.title} description={props.description} />
-      <SummaryListsBySections
+      <SummaryLists
         breadcrumbs={sortedBreadcrumbs}
         flow={props.flow}
         passport={props.passport}
         changeAnswer={props.changeAnswer}
         showChangeButton={props.showChangeButton}
         sectionComponent="h2"
+        disclaimer={props.disclaimer}
       />
       <PrintButton />
     </Card>
diff --git a/editor.planx.uk/src/@planx/components/Review/Public/Public.test.tsx b/editor.planx.uk/src/@planx/components/Review/Public/Public.test.tsx
index c04e555b1a..34df53cf38 100644
--- a/editor.planx.uk/src/@planx/components/Review/Public/Public.test.tsx
+++ b/editor.planx.uk/src/@planx/components/Review/Public/Public.test.tsx
@@ -5,6 +5,7 @@ import { setup } from "testUtils";
 import { vi } from "vitest";
 import { axe } from "vitest-axe";
 
+import { DEFAULT_REVIEW_DISCLAIMER } from "../model";
 import {
   breadcrumbsWithEmptySections,
   flowWithEmptySections,
@@ -42,6 +43,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={{}}
         breadcrumbs={{}}
         passport={{}}
@@ -67,6 +69,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer={DEFAULT_REVIEW_DISCLAIMER}
         flow={mockedFlow}
         breadcrumbs={mockedBreadcrumbs}
         passport={mockedPassport}
@@ -89,6 +92,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={mockedFlow}
         breadcrumbs={mockedBreadcrumbs}
         passport={mockedPassport}
@@ -108,6 +112,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={mockedFlow}
         breadcrumbs={mockedBreadcrumbs}
         passport={mockedPassport}
@@ -143,6 +148,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={mockedFlow}
         breadcrumbs={mockedBreadcrumbs}
         passport={mockedPassport}
@@ -178,6 +184,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={mockedFlow}
         breadcrumbs={mockedBreadcrumbs}
         passport={mockedPassport}
@@ -218,6 +225,7 @@ describe("File uploads", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={fileUploadFlow}
         breadcrumbs={fileUploadBreadcrumbs}
         passport={fileUploadPassport}
@@ -236,6 +244,7 @@ describe("File uploads", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={drawBoundaryFlow}
         breadcrumbs={uploadedPlansBreadcrumb}
         passport={uploadedPlansPassport}
@@ -270,6 +279,7 @@ describe("Flow with sections", () => {
       <Review
         title="Review with sections"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={flowWithSections}
         breadcrumbs={breadcrumbsWithSections}
         passport={passportWithSections}
@@ -297,6 +307,7 @@ describe("Flow with sections", () => {
       <Review
         title="Review with sections"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={flowWithSections}
         breadcrumbs={breadcrumbsWithSections}
         passport={passportWithSections}
@@ -328,6 +339,7 @@ describe("Flow with empty sections", () => {
       <Review
         title="Review with empty sections"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={flowWithEmptySections}
         breadcrumbs={breadcrumbsWithEmptySections}
         passport={passportWithEmptySections}
@@ -361,6 +373,7 @@ describe("Flow with empty sections", () => {
       <Review
         title="Review with empty sections"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
         flow={flowWithEmptySections}
         breadcrumbs={breadcrumbsWithEmptySections}
         passport={passportWithEmptySections}
diff --git a/editor.planx.uk/src/@planx/components/Review/Public/index.tsx b/editor.planx.uk/src/@planx/components/Review/Public/index.tsx
index 0295db3a9a..adacaed5fc 100644
--- a/editor.planx.uk/src/@planx/components/Review/Public/index.tsx
+++ b/editor.planx.uk/src/@planx/components/Review/Public/index.tsx
@@ -3,6 +3,7 @@ import { useStore } from "pages/FlowEditor/lib/store";
 import React from "react";
 
 import type { Review } from "../model";
+import { DEFAULT_REVIEW_DISCLAIMER } from "../model";
 import Presentational from "./Presentational";
 
 export default Component;
@@ -20,7 +21,8 @@ function Component(props: PublicProps<Review>) {
   return (
     <Presentational
       title={props.title}
-      description={props.description || ""}
+      description={props.description}
+      disclaimer={props.disclaimer || DEFAULT_REVIEW_DISCLAIMER}
       breadcrumbs={breadcrumbs}
       flow={flow}
       passport={passport}
diff --git a/editor.planx.uk/src/@planx/components/Review/Review.stories.tsx b/editor.planx.uk/src/@planx/components/Review/Review.stories.tsx
index ef5ae06e83..2cb48498d0 100644
--- a/editor.planx.uk/src/@planx/components/Review/Review.stories.tsx
+++ b/editor.planx.uk/src/@planx/components/Review/Review.stories.tsx
@@ -1,6 +1,7 @@
 import { Meta } from "@storybook/react";
 import React from "react";
 
+import { DEFAULT_REVIEW_DISCLAIMER } from "./model";
 import {
   breadcrumbsWithSections,
   flowWithSections,
@@ -29,6 +30,7 @@ export const Basic = () => {
     <Presentational
       title="Review"
       description="Check your answers before submitting"
+      disclaimer={DEFAULT_REVIEW_DISCLAIMER}
       breadcrumbs={mockedBreadcrumbs}
       flow={mockedFlow}
       passport={mockedPassport}
@@ -46,6 +48,7 @@ export const WithSections = () => {
     <Presentational
       title="Review"
       description="Check your answers before submitting"
+      disclaimer={DEFAULT_REVIEW_DISCLAIMER}
       breadcrumbs={breadcrumbsWithSections}
       flow={flowWithSections}
       passport={passportWithSections}
diff --git a/editor.planx.uk/src/@planx/components/Review/model.ts b/editor.planx.uk/src/@planx/components/Review/model.ts
index 842da0ad45..45d3f5aeae 100644
--- a/editor.planx.uk/src/@planx/components/Review/model.ts
+++ b/editor.planx.uk/src/@planx/components/Review/model.ts
@@ -3,6 +3,7 @@ import { BaseNodeData } from "../shared";
 export interface Review extends BaseNodeData {
   title: string;
   description: string;
+  disclaimer: string;
 }
 
 export const parseContent = (
@@ -10,5 +11,9 @@ export const parseContent = (
 ): Review => ({
   title: data?.title || "Check your answers before sending your application",
   description: data?.description || "",
+  disclaimer: data?.disclaimer || DEFAULT_REVIEW_DISCLAIMER,
   notes: data?.notes || "",
 });
+
+export const DEFAULT_REVIEW_DISCLAIMER =
+  "<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>";
diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx
index 11e0bd0113..649180332d 100644
--- a/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx
+++ b/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx
@@ -17,6 +17,7 @@ import { useAnalyticsTracking } from "pages/FlowEditor/lib/analytics/provider";
 import { Store, useStore } from "pages/FlowEditor/lib/store";
 import React, { useState } from "react";
 import { FONT_WEIGHT_SEMI_BOLD } from "theme";
+import ReactMarkdownOrHtml from "ui/shared/ReactMarkdownOrHtml/ReactMarkdownOrHtml";
 
 import {
   Field,
@@ -141,6 +142,7 @@ const presentationalComponents: {
 type BreadcrumbEntry = [NodeId, Store.Breadcrumbs];
 
 interface SummaryListBaseProps {
+  disclaimer?: string;
   flow: Store.Flow;
   passport: Store.Passport;
   changeAnswer: (id: NodeId) => void;
@@ -239,6 +241,7 @@ function SummaryListsBySections(props: SummaryListsBySectionsProps) {
                   passport={props.passport}
                   changeAnswer={props.changeAnswer}
                   showChangeButton={props.showChangeButton}
+                  disclaimer={props.disclaimer}
                 />
               </React.Fragment>
             ),
@@ -256,6 +259,7 @@ function SummaryListsBySections(props: SummaryListsBySectionsProps) {
         passport={props.passport}
         changeAnswer={props.changeAnswer}
         showChangeButton={props.showChangeButton}
+        disclaimer={props.disclaimer}
       />
     );
   }
@@ -331,19 +335,7 @@ function SummaryList(props: SummaryListProps) {
         confirmText="Yes"
         cancelText="No"
       >
-        <Typography>
-          Changing this answer means you will need to confirm any other answers
-          after it. This is because:
-          <ul>
-            <li>
-              a different answer might mean the service asks new questions
-            </li>
-            <li>
-              your planning officer needs the right information to assess your
-              application
-            </li>
-          </ul>
-        </Typography>
+        <ReactMarkdownOrHtml source={props.disclaimer}></ReactMarkdownOrHtml>
       </ConfirmationDialog>
     </>
   );


Reviews:
user: login: jessicamcinchak
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T10:27:47Z
user: login: augustlindemer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T11:12:39Z
user: login: jessicamcinchak
body: Sorry it's taken be a bit to come back to this one - looking great & very close ! One comment to resolve around handling default text.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T11:45:03Z
user: login: augustlindemer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T12:20:11Z
user: login: jessicamcinchak
body: Perfect, all working as expected - thanks again for picking this one up :slightly_smiling_face: 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-24T13:00:45Z

Review comments:
diff_hunk: @@ -331,19 +332,13 @@ function SummaryList(props: SummaryListProps) {
         confirmText="Yes"
         cancelText="No"
       >
-        <Typography>
-          Changing this answer means you will need to confirm any other answers
-          after it. This is because:
-          <ul>
-            <li>
-              a different answer might mean the service asks new questions
-            </li>
-            <li>
-              your planning officer needs the right information to assess your
-              application
-            </li>
-          </ul>
-        </Typography>
+        <ReactMarkdownOrHtml
+          // sketchy way to find the Review node in the flow to get the disclaimer, breaks if more than one review node in flow
+          source={
+            Object.values(props.flow).find((node) => node.type === TYPES.Review)
+              ?.data?.disclaimer
+          }
path: editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx
user: login: jessicamcinchak
body: Aha welcome to React land :dizzy: this is where the concept of "props" come in ! https://react.dev/learn/passing-props-to-a-component

So now, the top-level public/preview `Component` in `Review/Public/index.tsx` has access to `props.disclaimer` which you've defined in the editor & model.ts and, instead of `find`, we can pass this down the component tree to `SummaryList` (going through a `Presentational` component in the middle) ! 

So try something like :
1. From `Review/Public/index.tsx`, pass to `<Presentational disclaimer={props.disclaimer} ... />`
2. From `Review/Public/Presentation.tsx`, pass to `<SummaryListsBySection disclaimer={props.disclaimer} ... />`
3. Then here `source={props.disclaimer}` :slightly_smiling_face:
** expect to also update some `Props` type definitions along the way, otherwise components won't be expecting to receive/pass `disclaimer?: string;` :slightly_smiling_face: 
created_at: 2025-04-21T10:27:47Z
author_association: MEMBER
diff_hunk: @@ -20,7 +20,8 @@ function Component(props: PublicProps<Review>) {
   return (
     <Presentational
       title={props.title}
-      description={props.description || ""}
path: editor.planx.uk/src/@planx/components/Review/Public/index.tsx
user: login: augustlindemer
body: Redundant blank fallback removed, handled in model.
created_at: 2025-04-21T11:12:39Z
author_association: COLLABORATOR
diff_hunk: @@ -42,6 +42,7 @@ describe("Simple flow", () => {
       <Review
         title="Review"
         description="Check your answers before submitting"
+        disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
path: editor.planx.uk/src/@planx/components/Review/Public/Public.test.tsx
user: login: jessicamcinchak
body: nit: rather than repeating explicit text across each test setup, you could:
1. **export** `export const DEFAULT_REVIEW_DISCLAIMER` from model
2. import here
3. then define like `disclaimer={DEFAULT_REVIEW_DISCLAIMER}`

Will make it a little friendlier in the future if we ever update default text! 
created_at: 2025-04-24T11:37:06Z
author_association: MEMBER
diff_hunk: @@ -29,6 +29,7 @@ export const Basic = () => {
     <Presentational
       title="Review"
       description="Check your answers before submitting"
+      disclaimer="<p>Changing this answer means you will need to confirm any other answers after it. This is because: </p><ul><li>a different answer might mean the service asks new questions</li><li>your planning officer needs the right information to assess your application</li></ul></p>"
path: editor.planx.uk/src/@planx/components/Review/Review.stories.tsx
user: login: jessicamcinchak
body: Similar to above, may be convenient to import default from model.ts for Storybook! 
created_at: 2025-04-24T11:38:07Z
author_association: MEMBER
diff_hunk: @@ -20,7 +20,8 @@ function Component(props: PublicProps<Review>) {
   return (
     <Presentational
       title={props.title}
-      description={props.description || ""}
+      description={props.description}
+      disclaimer={props.disclaimer}
path: editor.planx.uk/src/@planx/components/Review/Public/index.tsx
user: login: jessicamcinchak
body: Here, I think we need to annoyingly & redundantly handle the fallback text _again_ as _existing_ Review components will not have a `disclaimer` prop currently! Eg `{props.disclaimer || DEFAULT_DISCLAIMER_TEXT}`. 

The fallback defined in `model.ts` correctly already handles Editor modals and _new_ public components, but not pre-existing public components! (Hence why we do content migrations in more complex scenarios to "add" props!)

Eg when I go through an existing LDC on the pizza _without_ updating the Review component and re-publishing, I get an empty modal: https://4601.planx.pizza/lambeth/apply-for-a-lawful-development-certificate/published?analytics=false&sessionId=04bb6cf3-80e7-4971-ae4f-b53d9ed2b589
![Screenshot from 2025-04-24 13-32-50](https://github.com/user-attachments/assets/da536921-2fe9-4cd4-a134-f24748ecaa30)


created_at: 2025-04-24T11:43:45Z
author_association: MEMBER
diff_hunk: @@ -20,7 +20,8 @@ function Component(props: PublicProps<Review>) {
   return (
     <Presentational
       title={props.title}
-      description={props.description || ""}
+      description={props.description}
+      disclaimer={props.disclaimer}
path: editor.planx.uk/src/@planx/components/Review/Public/index.tsx
user: login: augustlindemer
body: Thanks! With the edits existing review pages now correctly fetch the default disclaimer.
![image](https://github.com/user-attachments/assets/9c5fc39c-cc5c-49d8-8f94-2365ed0c4555)

created_at: 2025-04-24T12:20:11Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-20T16:35:18Z
body: Removed vultr server and associated DNS entries
<!-- Sticky Pull Request Commentvultr -->
author_association: NONE
user: login: augustlindemer
created_at: 2025-04-21T09:55:17Z
body: ![](https://github.trello.services/images/mini-trello-icon.png) [Make Review component 'Change' warning editable](https://trello.com/c/Dilh77dX/3284-make-review-component-change-warning-editable)
author_association: COLLABORATOR