Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: editors can select which planning constraints to check #3968

Merged
merged 16 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Typography from "@mui/material/Typography";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { EditorProps } from "@planx/components/shared/types";
import { useFormik } from "formik";
import React from "react";
import React, { ChangeEvent } from "react";
import { FONT_WEIGHT_BOLD } from "theme";
import InputGroup from "ui/editor/InputGroup";
import { ModalFooter } from "ui/editor/ModalFooter";
Expand All @@ -36,8 +36,38 @@ function PlanningConstraintsComponent(props: Props) {
data: newValues,
});
},
validate: () => {
// TODO must select at least one formik.values.dataValues
},
});

const changeSelectAll =
(vals: string[] | undefined) =>
(_event: ChangeEvent<HTMLInputElement>, _checked: boolean) => {
let newCheckedVals: string[];
if (vals?.length !== availableDatasets.length) {
newCheckedVals = availableDatasets.map((d) => d.val);
} else {
newCheckedVals = [];
}

formik.setFieldValue("dataValues", newCheckedVals);
};

const changeDataset =
(val: string) =>
(_event: ChangeEvent<HTMLInputElement>, _checked: boolean) => {
let newCheckedVals;
if (formik.values.dataValues?.includes(val)) {
newCheckedVals = formik.values.dataValues.filter((dv) => dv !== val);
} else {
newCheckedVals = formik.values.dataValues
? [...formik.values.dataValues, val]
: [val];
}
formik.setFieldValue("dataValues", newCheckedVals);
};

return (
<form onSubmit={formik.handleSubmit} id="modal">
<ModalSection>
Expand Down Expand Up @@ -91,8 +121,11 @@ function PlanningConstraintsComponent(props: Props) {
<TableCell>
<Checkbox
color="primary"
checked={true}
disabled={true}
checked={
availableDatasets.length ===
formik.values.dataValues?.length
}
onChange={changeSelectAll(formik.values.dataValues)}
inputProps={{
"aria-label": "select all constraints",
}}
Expand All @@ -111,8 +144,10 @@ function PlanningConstraintsComponent(props: Props) {
<TableCell sx={{ verticalAlign: "top" }}>
<Checkbox
color="primary"
checked={true}
disabled={true}
checked={formik.values.dataValues?.includes(
dataset.val,
)}
onChange={changeDataset(dataset.val)}
/>
</TableCell>
<TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { stringify } from "wkt";
import { SiteAddress } from "../FindProperty/model";
import { ErrorSummaryContainer } from "../shared/Preview/ErrorSummaryContainer";
import {
availableDatasets,
type IntersectingConstraints,
type PlanningConstraints,
} from "./model";
Expand All @@ -36,6 +37,9 @@ export type InaccurateConstraints =
export default Component;

function Component(props: Props) {
// Existing components will not have dataValues prop so should default to all available datasets
const dataValues = props.dataValues || availableDatasets.map((d) => d.val);
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved

const [
currentCardId,
cachedBreadcrumbs,
Expand All @@ -56,8 +60,7 @@ function Component(props: Props) {

// PlanningConstraints must come after at least a FindProperty in the graph
const showGraphError = !longitude || !latitude;
if (showGraphError)
throw new GraphError("mapInputFieldMustFollowFindProperty");
if (showGraphError) throw new GraphError("nodeMustFollowFindProperty");
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved

// Even though this component will fetch fresh GIS data when coming "back",
// still prepopulate any previously marked inaccurateConstraints
Expand All @@ -71,79 +74,79 @@ function Component(props: Props) {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());

const fetcher: Fetcher<GISResponse> = (url: string) =>
fetch(url).then((r) => r.json());

// Get the WKT representation of the site boundary drawing or address point to pass to Planning Data
const wktPoint = `POINT(${longitude} ${latitude})`;
const wktPolygon: string | undefined =
siteBoundary && stringify(siteBoundary);

const planningDataParams: Record<string, string> = {
geom: wktPolygon || wktPoint,
...params,
};

// Fetch planning constraints data for a given local authority
const root = `${import.meta.env.VITE_APP_API_URL}/gis/${teamSlug}?`;
// Fetch planning constraints data for a given local authority if Planning Data & a geometry is available
const shouldFetchPlanningData =
hasPlanningData &&
latitude &&
longitude &&
dataValues.some((val) => val !== "road.classified");
const teamGisEndpoint: string =
root +
new URLSearchParams(
hasPlanningData ? planningDataParams : undefined,
).toString();

const fetcher: Fetcher<GISResponse | GISResponse["constraints"]> = (
url: string,
) => fetch(url).then((r) => r.json());
`${import.meta.env.VITE_APP_API_URL}/gis/${teamSlug}?` +
new URLSearchParams(planningDataParams).toString();
const {
data,
mutate,
error: dataError,
isValidating,
} = useSWR(() => (latitude && longitude ? teamGisEndpoint : null), fetcher, {
revalidateOnFocus: false,
});
} = useSWR(
() => (shouldFetchPlanningData ? teamGisEndpoint : null),
fetcher,
{ revalidateOnFocus: false },
);

// If an OS address was selected, additionally fetch classified roads (available nationally) using the USRN identifier,
// skip if the applicant plotted a new non-UPRN address on the map
const shouldFetchRoads =
hasPlanningData && usrn && dataValues.includes("road.classified");
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
const classifiedRoadsEndpoint: string =
`${import.meta.env.VITE_APP_API_URL}/roads?` +
new URLSearchParams(usrn ? { usrn } : undefined)?.toString();

const {
data: roads,
error: roadsError,
isValidating: isValidatingRoads,
} = useSWR(
() => (usrn && hasPlanningData ? classifiedRoadsEndpoint : null),
() => (shouldFetchRoads ? classifiedRoadsEndpoint : null),
fetcher,
{ revalidateOnFocus: false },
);

// Merge Planning Data and Roads responses for a unified list of constraints
const constraints: GISResponse["constraints"] | Record<string, any> = {
const constraints: GISResponse["constraints"] = {
...data?.constraints,
...roads?.constraints,
};

const metadata: GISResponse["metadata"] | Record<string, any> = {
const metadata: GISResponse["metadata"] = {
...data?.metadata,
...roads?.metadata,
};

const handleSubmit = () => {
// `_constraints` & `_overrides` are responsible for auditing
const _constraints: Array<
EnhancedGISResponse | GISResponse["constraints"]
> = [];
const _constraints: Array<EnhancedGISResponse> = [];
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
if (hasPlanningData) {
if (data && !dataError)
_constraints.push({
...data,
planxRequest: teamGisEndpoint,
} as EnhancedGISResponse);
});
if (roads && !roadsError)
_constraints.push({
...roads,
planxRequest: classifiedRoadsEndpoint,
} as EnhancedGISResponse);
});
}

const hasInaccurateConstraints =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface PlanningConstraints extends BaseNodeData {
description: string;
fn: string;
disclaimer: string;
dataValues?: string[] | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe datasets is more meaningful here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a fallback way of handling "all" values?

dataValues?: string[] | "all"

If Planning Data add a new dataset, it would be pretty cumbersome to have to get all services to toggle it on. I think Editors would think that ticking the "main" checkbox means "all datasets" not just "all currently listed".

Alternatively if we could easily update all PlanningConstraints nodes with any new dataset this would also be solved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel quite strongly that datasets is NOT a good name because we're not allowing users to say "I only want to check TPO orders dataset from Planning Data, not TPO zones" - rather we're letting them select/deselect our pre-curated "options" (each with a title, passport value, and associated source datasets).

Perhaps options is more generic and easier to grasp here though? In my mind, they're basically the PlanningConstraint node's equivalent of Question/Checklist "edges"/"options" ?

Copy link
Member Author

@jessicamcinchak jessicamcinchak Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the "all" question - I think this becomes much simpler in a word of Templates (eg PlanningConstraints will not be a "placeholder" node therefore just updated once per service template & changes propagated to children on publish?), and if still too cumbersome then would favor data migration script like you say!

Also no near-future requests for more datasets from Planning Data besides Planning History which I think will have a strong argument for it's own component or API outside of existing /gis.

}

export const parseContent = (
Expand All @@ -16,6 +17,7 @@ export const parseContent = (
"Planning constraints might limit how you can develop or use the property",
fn: data?.fn || DEFAULT_FN,
disclaimer: data?.disclaimer || DEFAULT_PLANNING_CONDITIONS_DISCLAIMER,
dataValues: data?.dataValues || availableDatasets.map((d) => d.val),
...parseBaseNodeData(data),
});

Expand Down
Loading