From 48a4be0438a7de5dd0f8d525ab7a51c35ee2b971 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 13 May 2024 16:23:10 +0200 Subject: [PATCH 1/3] always enable continue button on findproperty map page --- .../components/FindProperty/Public/Map.tsx | 133 ++++++++++------- .../FindProperty/Public/Public.test.tsx | 15 +- .../components/FindProperty/Public/index.tsx | 136 +++++++++--------- 3 files changed, 161 insertions(+), 123 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/Map.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/Map.tsx index 90978ee9d6..1ad63c2512 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/Map.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/Map.tsx @@ -11,10 +11,10 @@ import { MapContainer, MapFooter, } from "@planx/components/shared/Preview/MapContainer"; -import { GeoJSONObject } from "@turf/helpers"; import { useStore } from "pages/FlowEditor/lib/store"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import InputLabel from "ui/public/InputLabel"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; import Input from "ui/shared/Input"; import { DEFAULT_NEW_ADDRESS_LABEL, SiteAddress } from "../model"; @@ -23,10 +23,15 @@ interface PlotNewAddressProps { setAddress: React.Dispatch>; setPage: React.Dispatch>; initialProposedAddress?: SiteAddress; - boundary?: GeoJSONObject | undefined; id?: string; description?: string; descriptionLabel?: string; + mapValidationError?: string; + setMapValidationError: React.Dispatch< + React.SetStateAction + >; + showSiteDescriptionError: boolean; + setShowSiteDescriptionError: React.Dispatch>; } type Coordinates = { @@ -55,8 +60,6 @@ export default function PlotNewAddress(props: PlotNewAddressProps): FCReturn { const [siteDescription, setSiteDescription] = useState( props.initialProposedAddress?.title ?? null, ); - const [showSiteDescriptionError, setShowSiteDescriptionError] = - useState(false); const [environment, boundaryBBox] = useStore((state) => [ state.previewEnvironment, @@ -85,6 +88,17 @@ export default function PlotNewAddress(props: PlotNewAddressProps): FCReturn { }; }, [setCoordinates]); + const mapValidationErrorRef = useRef(props.mapValidationError); + useEffect(() => { + mapValidationErrorRef.current = props.mapValidationError; + }, [props.mapValidationError]); + + useEffect(() => { + if (mapValidationErrorRef.current) { + props.setMapValidationError(undefined); + } + }, [coordinates]); + useEffect(() => { // when we have all required address parts, call setAddress to enable the "Continue" button if (siteDescription && coordinates) { @@ -99,7 +113,7 @@ export default function PlotNewAddress(props: PlotNewAddressProps): FCReturn { }, [coordinates, siteDescription]); const handleSiteDescriptionCheck = () => { - if (!siteDescription) setShowSiteDescriptionError(true); + if (!siteDescription) props.setShowSiteDescriptionError(true); }; const handleSiteDescriptionInputChange = ( @@ -111,54 +125,65 @@ export default function PlotNewAddress(props: PlotNewAddressProps): FCReturn { return ( <> - -

- An interactive map centred on the local authority district, showing - the Ordnance Survey basemap. Click to place a point representing your - proposed site location. -

- {/* @ts-ignore */} - - + + +

+ An interactive map centred on the local authority district, showing + the Ordnance Survey basemap. Click to place a point representing + your proposed site location. +

+ {/* @ts-ignore */} + +
+
+ + + The coordinate location of your address point is:{" "} + + {`${ + (coordinates?.x && Math.round(coordinates.x)) || 0 + } Easting (X), ${ + (coordinates?.y && Math.round(coordinates.y)) || 0 + } Northing (Y)`} + + + { + props.setPage("os-address"); + props.setAddress(undefined); + }} + > - The coordinate location of your address point is:{" "} - - {`${ - (coordinates?.x && Math.round(coordinates.x)) || 0 - } Easting (X), ${ - (coordinates?.y && Math.round(coordinates.y)) || 0 - } Northing (Y)`} - + I want to select an existing address - { - props.setPage("os-address"); - props.setAddress(undefined); - }} - > - - I want to select an existing address - - - -
+ + { const descriptionInput = screen.getByTestId("new-address-input"); expect(descriptionInput).toBeInTheDocument(); - // expect continue to be disabled because an address has not been selected - expect(screen.getByTestId("continue-button")).toBeDisabled(); + // Continue button is always enabled, but validation prevents submit until we have complete address details + expect(screen.getByTestId("continue-button")).toBeEnabled(); + await user.click(screen.getByTestId("continue-button")); expect(handleSubmit).not.toHaveBeenCalled(); }); @@ -416,9 +417,15 @@ describe("plotting a new address that does not have a uprn yet", () => { screen.getByText(`Enter a site description such as "Land at..."`), ).toBeInTheDocument(); - // expect continue to be disabled because we have incomplete address details - expect(screen.getByTestId("continue-button")).toBeDisabled(); + // Continue button is always enabled, but validation prevents submit until we have complete address details + expect(screen.getByTestId("continue-button")).toBeEnabled(); + await user.click(screen.getByTestId("continue-button")); expect(handleSubmit).not.toHaveBeenCalled(); + + // continue should trigger map error wrapper too + expect( + screen.getByTestId("error-message-plot-new-address-map"), + ).toBeInTheDocument(); }); it("recovers previously submitted address when clicking the back button and lands on the map page", async () => { diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx index 02307ff308..10bdab7330 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx @@ -7,9 +7,9 @@ import QuestionHeader from "@planx/components/shared/Preview/QuestionHeader"; import { squareMetresToHectares } from "@planx/components/shared/utils"; import { PublicProps } from "@planx/components/ui"; import area from "@turf/area"; -import { Feature, GeoJSONObject } from "@turf/helpers"; +import { Feature } from "@turf/helpers"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; -import { Store, useStore } from "pages/FlowEditor/lib/store"; +import { Store } from "pages/FlowEditor/lib/store"; import React, { useEffect, useState } from "react"; import useSWR from "swr"; import ExternalPlanningSiteDialog, { @@ -51,6 +51,10 @@ function Component(props: Props) { : "os-address"; const [page, setPage] = useState<"os-address" | "new-address">(startPage); + const [mapValidationError, setMapValidationError] = useState(); + const [showSiteDescriptionError, setShowSiteDescriptionError] = + useState(false); + const [address, setAddress] = useState( previouslySubmittedData?._address, ); @@ -59,9 +63,6 @@ function Component(props: Props) { >(); const [regions, setRegions] = useState(); const [titleBoundary, setTitleBoundary] = useState(); - const [boundary, setBoundary] = useState(); - - const teamSettings = useStore((state) => state.teamSettings); // Use the address point to fetch the Local Authority District(s) & region via Digital Land const options = new URLSearchParams({ @@ -89,21 +90,6 @@ function Component(props: Props) { }, ); - // if allowNewAddresses is on, fetch the boundary geojson for this team to position the map view or default to London - // example value for team.settings.boundary is https://www.planning.data.gov.uk/entity/8600093.geojson - const { data: geojson } = useSWR( - () => - props.allowNewAddresses && teamSettings?.boundary - ? teamSettings.boundary - : null, - fetcher, - { - shouldRetryOnError: true, - errorRetryInterval: 500, - errorRetryCount: 1, - }, - ); - useEffect(() => { if (address && data?.features?.length > 0) { const lads: string[] = []; @@ -124,11 +110,62 @@ function Component(props: Props) { } }, [data, address]); - useEffect(() => { - if (geojson) setBoundary(geojson); - }, [geojson]); + const validateAndSubmit = () => { + // TODO `if (isValidating)` on either page, wrap Continue button in error mesage? + + if (page === "new-address") { + if (address?.x === undefined && address?.y === undefined) + setMapValidationError("Click or tap to place a point on the map"); + + if (address?.title === undefined) setShowSiteDescriptionError(true); + } + + if (address) { + const newPassportData: Store.userData["data"] = {}; + newPassportData["_address"] = address; + if (address?.planx_value) { + newPassportData["property.type"] = [address.planx_value]; + } + + if (localAuthorityDistricts) { + newPassportData["property.localAuthorityDistrict"] = + localAuthorityDistricts; + } + if (regions) { + newPassportData["property.region"] = regions; + } + if (titleBoundary) { + const areaSquareMetres = + Math.round(area(titleBoundary as Feature) * 100) / 100; + newPassportData["property.boundary.title"] = titleBoundary; + newPassportData["property.boundary.title.area"] = areaSquareMetres; + newPassportData["property.boundary.title.area.hectares"] = + squareMetresToHectares(areaSquareMetres); + } + + newPassportData[PASSPORT_COMPONENT_ACTION_KEY] = + address?.source === "os" + ? FindPropertyUserAction.Existing + : FindPropertyUserAction.New; + + props.handleSubmit?.({ data: { ...newPassportData } }); + } + }; + + return ( + + {getBody()} + + ); - function getPageBody() { + function getBody() { if (props.allowNewAddresses && page === "new-address") { return ( <> @@ -143,11 +180,20 @@ function Component(props: Props) { previouslySubmittedData?._address?.source === "proposed" && previouslySubmittedData?._address } - boundary={boundary} id={props.id} description={props.newAddressDescription || ""} descriptionLabel={props.newAddressDescriptionLabel || ""} + mapValidationError={mapValidationError} + setMapValidationError={setMapValidationError} + showSiteDescriptionError={showSiteDescriptionError} + setShowSiteDescriptionError={setShowSiteDescriptionError} /> + {Boolean(address) && isValidating && ( + + )} ); } else { @@ -200,44 +246,4 @@ function Component(props: Props) { ); } } - - return ( - { - if (address) { - const newPassportData: Store.userData["data"] = {}; - newPassportData["_address"] = address; - if (address?.planx_value) { - newPassportData["property.type"] = [address.planx_value]; - } - - if (localAuthorityDistricts) { - newPassportData["property.localAuthorityDistrict"] = - localAuthorityDistricts; - } - if (regions) { - newPassportData["property.region"] = regions; - } - if (titleBoundary) { - const areaSquareMetres = - Math.round(area(titleBoundary as Feature) * 100) / 100; - newPassportData["property.boundary.title"] = titleBoundary; - newPassportData["property.boundary.title.area"] = areaSquareMetres; - newPassportData["property.boundary.title.area.hectares"] = - squareMetresToHectares(areaSquareMetres); - } - - newPassportData[PASSPORT_COMPONENT_ACTION_KEY] = - address?.source === "os" - ? FindPropertyUserAction.Existing - : FindPropertyUserAction.New; - - props.handleSubmit?.({ data: { ...newPassportData } }); - } - }} - > - {getPageBody()} - - ); } From b2b0e034e9495bfce80a1e6528adf9ccc24ed77a Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 13 May 2024 21:06:27 +0200 Subject: [PATCH 2/3] wip --- .../FindProperty/Public/Autocomplete.tsx | 42 ++++++++++++------- .../FindProperty/Public/Public.test.tsx | 27 ++++++++++-- .../components/FindProperty/Public/index.tsx | 31 ++++++++++---- .../@planx/components/shared/Preview/Card.tsx | 27 ++++++------ 4 files changed, 88 insertions(+), 39 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/Autocomplete.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/Autocomplete.tsx index 9dc263a464..f6e53b8290 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/Autocomplete.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/Autocomplete.tsx @@ -10,6 +10,7 @@ import find from "lodash/find"; import { parse, toNormalised } from "postcode"; import React, { useEffect, useState } from "react"; import InputLabel from "ui/public/InputLabel"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; import Input from "ui/shared/Input"; import type { SiteAddress } from "../model"; @@ -25,6 +26,12 @@ interface PickOSAddressProps { initialSelectedAddress?: Option; id?: string; description?: string; + showPostcodeError: boolean; + setShowPostcodeError: React.Dispatch>; + addressAutocompleteError?: string; + setAddressAutocompleteError: React.Dispatch< + React.SetStateAction + >; } const AutocompleteWrapper = styled(Box)(({ theme }) => ({ @@ -55,7 +62,6 @@ export default function PickOSAddress(props: PickOSAddressProps): FCReturn { const [selectedOption, setSelectedOption] = useState {sanitizedPostcode && ( - /* @ts-ignore */ - + + {/* @ts-ignore */} + + )} ); diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx index 7e5dee7686..cb6e25089d 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx @@ -159,8 +159,14 @@ describe("render states", () => { expect(autocomplete.getAttribute("postcode")).toEqual("SE5 0HU"); expect(autocomplete.getAttribute("initialAddress")).toBeFalsy(); - // expect continue to be disabled because an address has not been selected - expect(screen.getByTestId("continue-button")).toBeDisabled(); + // user is unable to continue and to submit incomplete data + const continueButton = screen.getByTestId("continue-button"); + expect(continueButton).toBeEnabled(); + await user.click(continueButton); + + expect( + screen.getByTestId("error-message-address-autocomplete"), + ).toBeInTheDocument(); expect(handleSubmit).not.toHaveBeenCalled(); }); @@ -233,7 +239,11 @@ describe("render states", () => { await screen.findByText("Type your postal code"), ).toBeInTheDocument(); - expect(screen.getByTestId("continue-button")).toBeDisabled(); + // user is unable to continue and submit incomplete data + expect(screen.getByTestId("continue-button")).toBeEnabled(); + await user.click(screen.getByTestId("continue-button")); + + expect(screen.getByText("Enter a valid UK postcode")).toBeInTheDocument(); expect(handleSubmit).not.toHaveBeenCalled(); }); @@ -316,11 +326,14 @@ describe("picking an OS address", () => { }); it("updates the address-autocomplete props when the postcode is changed", async () => { + const handleSubmit = jest.fn(); + const { user } = setup( , ); @@ -351,7 +364,13 @@ describe("picking an OS address", () => { // User is unable to continue and to submit incomplete data const continueButton = screen.getByTestId("continue-button"); - expect(continueButton).toBeDisabled(); + expect(continueButton).toBeEnabled(); + await user.click(continueButton); + + expect( + screen.getByTestId("error-message-address-autocomplete"), + ).toBeInTheDocument(); + expect(handleSubmit).not.toHaveBeenCalled(); }); it("recovers previously submitted address when clicking the back button", async () => { diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx index 10bdab7330..01406a4014 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx @@ -51,9 +51,14 @@ function Component(props: Props) { : "os-address"; const [page, setPage] = useState<"os-address" | "new-address">(startPage); + const [showPostcodeError, setShowPostcodeError] = useState(false); + const [addressAutocompleteError, setAddressAutocompleteError] = + useState(); const [mapValidationError, setMapValidationError] = useState(); const [showSiteDescriptionError, setShowSiteDescriptionError] = useState(false); + const [dataFetchError, setDataFetchError] = + useState("This is a test"); const [address, setAddress] = useState( previouslySubmittedData?._address, @@ -111,7 +116,16 @@ function Component(props: Props) { }, [data, address]); const validateAndSubmit = () => { - // TODO `if (isValidating)` on either page, wrap Continue button in error mesage? + if (isValidating) { + setDataFetchError("Please wait for data fetching to complete"); + } + + if (page === "os-address") { + if (address?.postcode === undefined) setShowPostcodeError(true); + + if (address?.title === undefined) + setAddressAutocompleteError("Select an address"); + } if (page === "new-address") { if (address?.x === undefined && address?.y === undefined) @@ -155,11 +169,8 @@ function Component(props: Props) { return ( {getBody()} @@ -190,7 +201,7 @@ function Component(props: Props) { /> {Boolean(address) && isValidating && ( )} @@ -216,6 +227,10 @@ function Component(props: Props) { } id={props.id} description={props.description || ""} + showPostcodeError={showPostcodeError} + setShowPostcodeError={setShowPostcodeError} + addressAutocompleteError={addressAutocompleteError} + setAddressAutocompleteError={setAddressAutocompleteError} /> {!props.allowNewAddresses ? ( )} diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx index 6eb6ba8dda..96b340db24 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/Card.tsx @@ -7,6 +7,7 @@ import { useAnalyticsTracking } from "pages/FlowEditor/lib/analytics/provider"; import { useStore } from "pages/FlowEditor/lib/store"; import React, { useEffect } from "react"; import { ApplicationPath } from "types"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; import SaveResumeButton from "./SaveResumeButton"; @@ -14,6 +15,7 @@ interface Props { children: React.ReactNode; isValid?: boolean; handleSubmit?: (data?: any) => void; + error?: string; } export const contentFlowSpacing = (theme: Theme): React.CSSProperties => ({ @@ -78,20 +80,21 @@ const Card: React.FC = ({ {...props} > {children} - {handleSubmit && ( - + + + )} {showSaveResumeButton && } From 9a5ab948b8ed962fa6c0984cbc32d01b8f61de80 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 13 May 2024 21:32:46 +0200 Subject: [PATCH 3/3] fix default error msg --- .../src/@planx/components/FindProperty/Public/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx index 01406a4014..6be079f44e 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx @@ -57,8 +57,7 @@ function Component(props: Props) { const [mapValidationError, setMapValidationError] = useState(); const [showSiteDescriptionError, setShowSiteDescriptionError] = useState(false); - const [dataFetchError, setDataFetchError] = - useState("This is a test"); + const [dataFetchError, setDataFetchError] = useState(); const [address, setAddress] = useState( previouslySubmittedData?._address,