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 9 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
34 changes: 25 additions & 9 deletions api.planx.uk/modules/gis/service/digitalLand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,30 @@
geom: string,
extras: Record<string, string>,
): Promise<GISResponse> {
// generate list of digital land datasets we should query based on 'active' planx schema variables
// generate list of digital land datasets we should query and their associated passport values
const activeDatasets: string[] = [];
Object.keys(baseSchema).forEach((key) => {
if (baseSchema[key]["active"]) {
baseSchema[key]["digital-land-datasets"]?.forEach((dataset: string) => {
activeDatasets.push(dataset);
});
}
});
let activeDataValues: string[] = [];
if (extras?.vals) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's tricky as these aren't 1:1 Planning.data datasets, but I think we should use a more meaningful param name here - maybe datasets, but this isn't ideal given the context here and the existing meaning datasets has.

We also need to add this new param to Swagger docs 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Went with vals because the response includes a val property for each item - agree not most semantically meaningful though ! Continue to strongly oppose "datasets" though as that already corresponds to Planning Data source, not our curated constraint item which rely on one or many source datasets.

// if the editor selected constraints, prioritise their selection
activeDataValues = extras.vals.split(",");
Object.keys(baseSchema).forEach((key) => {
if (activeDataValues.includes(key)) {
baseSchema[key]["digital-land-datasets"]?.forEach((dataset: string) => {
activeDatasets.push(dataset);
});
}
});
} else {
// else default to the internally maintained list of all "active" datasets
Object.keys(baseSchema).forEach((key) => {
if (baseSchema[key]["active"]) {
activeDataValues.push(key);
baseSchema[key]["digital-land-datasets"]?.forEach((dataset: string) => {
activeDatasets.push(dataset);
});
}
});
}
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved

// set up request query params per https://www.planning.data.gov.uk/docs
const options = {
Expand All @@ -97,8 +112,8 @@
options,
)}${datasets}`;
const res = await fetch(url)
.then((response: { json: () => any }) => response.json())

Check warning on line 115 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
.catch((error: any) => console.log(error));

Check warning on line 116 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type

// if analytics are "on", store an audit record of the raw response
if (extras?.analytics !== "false") {
Expand Down Expand Up @@ -132,7 +147,7 @@
// check for & add any 'positive' constraints to the formattedResult
let formattedResult: Record<string, Constraint> = {};
if (res && res.count > 0 && res.entities) {
res.entities.forEach((entity: { dataset: any }) => {

Check warning on line 150 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
// get the planx variable that corresponds to this entity's 'dataset', should never be null because our initial request is filtered on 'dataset'
const key = Object.keys(baseSchema).find((key) =>
baseSchema[key]["digital-land-datasets"]?.includes(entity.dataset),
Expand All @@ -159,7 +174,8 @@
// TODO followup with digital land about how to return 'nots' via API (currently assumes any "active" metadata was successfully queried)
const nots = Object.keys(baseSchema).filter(
(key) =>
baseSchema[key]["active"] && !Object.keys(formattedResult).includes(key),
activeDataValues.includes(key) &&
!Object.keys(formattedResult).includes(key),
);
nots.forEach((not) => {
formattedResult[not] = {
Expand All @@ -180,7 +196,7 @@
formattedResult["designated.nationalPark"] &&
formattedResult["designated.nationalPark"].value
) {
formattedResult["designated.nationalPark"]?.data?.forEach((entity: any) => {

Check warning on line 199 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
if (
baseSchema[broads]["digital-land-entities"]?.includes(entity.entity)
) {
Expand Down Expand Up @@ -237,7 +253,7 @@

// loop through any intersecting a4 data entities and set granular planx values based on this local authority's schema
if (a4s && formattedResult["article4"].value) {
formattedResult["article4"]?.data?.forEach((entity: any) => {

Check warning on line 256 in api.planx.uk/modules/gis/service/digitalLand.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Unexpected any. Specify a different type
Object.keys(a4s)?.forEach((key) => {
if (
// these are various ways we link source data to granular planx values (see local_authorities/metadata for specifics)
Expand Down
187 changes: 114 additions & 73 deletions editor.planx.uk/src/@planx/components/PlanningConstraints/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import TableRow from "@mui/material/TableRow";
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 { FormikErrors, useFormik } from "formik";
import React, { ChangeEvent } from "react";
import { FONT_WEIGHT_BOLD } from "theme";
import InputGroup from "ui/editor/InputGroup";
import { ModalFooter } from "ui/editor/ModalFooter";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import RichTextInput from "ui/editor/RichTextInput/RichTextInput";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";

Expand All @@ -36,8 +37,41 @@ function PlanningConstraintsComponent(props: Props) {
data: newValues,
});
},
validate: (values) => {
const errors: FormikErrors<PlanningConstraints> = {};
if (!values.dataValues || values.dataValues?.length < 1) {
errors.dataValues = "Select at least one constraint";
}
return errors;
},
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
});

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 @@ -84,77 +118,84 @@ function PlanningConstraintsComponent(props: Props) {
</InputRow>
</InputGroup>
<InputGroup label="Which constraints do you want to check?">
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>
<Checkbox
color="primary"
checked={true}
disabled={true}
inputProps={{
"aria-label": "select all constraints",
}}
/>
</TableCell>
<TableCell sx={{ fontWeight: FONT_WEIGHT_BOLD }}>
Constraints
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{availableDatasets
.sort((a, b) => a.val.localeCompare(b.val))
.map((dataset, i: number) => (
<TableRow key={i}>
<TableCell sx={{ verticalAlign: "top" }}>
<Checkbox
color="primary"
checked={true}
disabled={true}
/>
</TableCell>
<TableCell>
<InputGroup>
<InputRow>
<Input
format="large"
name="text"
value={dataset.text}
disabled
/>
</InputRow>
<InputRow>
<Input
format="data"
name="val"
value={dataset.val}
disabled
/>
</InputRow>
</InputGroup>
<Box>
<Typography
variant="body2"
color="GrayText"
sx={{ fontSize: ".8em", pb: 1 }}
>
{`via ${dataset.source}: ${dataset.datasets
.filter(Boolean)
.join(", ")} ${
dataset?.entity
? `(entity ${dataset.entity})`
: ``
}`}
</Typography>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<ErrorWrapper error={formik.errors.dataValues}>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>
<Checkbox
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
color="primary"
checked={
availableDatasets.length ===
formik.values.dataValues?.length
}
onChange={changeSelectAll(formik.values.dataValues)}
inputProps={{
"aria-label": "select all constraints",
}}
/>
</TableCell>
<TableCell sx={{ fontWeight: FONT_WEIGHT_BOLD }}>
Constraints
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{availableDatasets
.sort((a, b) => a.val.localeCompare(b.val))
.map((dataset, i: number) => (
<TableRow key={i}>
<TableCell sx={{ verticalAlign: "top" }}>
<Checkbox
color="primary"
checked={formik.values.dataValues?.includes(
dataset.val,
)}
onChange={changeDataset(dataset.val)}
/>
</TableCell>
<TableCell>
<InputGroup>
<InputRow>
<Input
format="large"
name="text"
value={dataset.text}
disabled
/>
</InputRow>
<InputRow>
<Input
format="data"
name="val"
value={dataset.val}
disabled
/>
</InputRow>
</InputGroup>
<Box>
<Typography
variant="body2"
color="GrayText"
sx={{ fontSize: ".8em", pb: 1 }}
>
{`via ${dataset.source}: ${dataset.datasets
.filter(Boolean)
.join(", ")} ${
dataset?.entity
? `(entity ${dataset.entity})`
: ``
}`}
</Typography>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</ErrorWrapper>
</InputGroup>
<InputGroup label="Planning conditions disclaimer">
<InputRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
simpleBreadcrumbs,
simpleFlow,
} from "./mocks/simpleFlow";
import { availableDatasets } from "./model";
import PlanningConstraints from "./Public";

const { setState } = useStore;
Expand Down Expand Up @@ -259,6 +260,86 @@ describe("following a FindProperty component", () => {
});
});

describe("selectable datasets in editor", () => {
beforeEach(() => {
act(() =>
setState({
breadcrumbs: simpleBreadcrumbs,
flow: simpleFlow,
teamIntegrations: {
hasPlanningData: true,
},
}),
);
});

it("does not initiate `/roads` request when `road.classified` is not selected by an editor", async () => {
setup(
<PlanningConstraints
title="Planning constraints"
description="Things that might affect your project"
fn="property.constraints.planning"
disclaimer="This page does not include information about historic planning conditions that may apply to this property."
handleSubmit={vi.fn()}
dataValues={availableDatasets
.filter((d) => d.val !== "road.classified")
.map((d) => d.val)}
/>,
);

expect(swr).toHaveBeenCalled();

// Planning constraints API still called
const planingConstraintsURL = (
vi.mocked(useSWR).mock.calls[0][0] as () => {}
)();
const planingConstraintsResponse = vi.mocked(useSWR).mock.results[0].value;

expect(planingConstraintsURL).toContain("/gis");
expect(planingConstraintsResponse).toEqual({
data: digitalLandResponseMock,
});

// Roads API not called due to missing `road.classified` data value
const roadsURL = (vi.mocked(useSWR).mock.calls[1][0] as () => {})();
const roadsResponse = vi.mocked(useSWR).mock.results[1].value;

expect(roadsURL).toBeNull();
expect(roadsResponse).toEqual({ data: null });
});

it("does not initiate `/gis/:localAuthority` request when only `road.classified` is selected by an editor", async () => {
setup(
<PlanningConstraints
title="Planning constraints"
description="Things that might affect your project"
fn="property.constraints.planning"
disclaimer="This page does not include information about historic planning conditions that may apply to this property."
handleSubmit={vi.fn()}
dataValues={["road.classified"]}
/>,
);

expect(swr).toHaveBeenCalled();

// Roads API is called
const roadsURL = (vi.mocked(useSWR).mock.calls[1][0] as () => {})();
const roadsResponse = vi.mocked(useSWR).mock.results[1].value;

expect(roadsURL).toContain("/roads");
expect(roadsResponse).toEqual({ data: classifiedRoadsResponseMock });

// Planning constraints API not called due to missing data values
const planingConstraintsURL = (
vi.mocked(useSWR).mock.calls[0][0] as () => {}
)();
const planningConstraintsResponse = vi.mocked(useSWR).mock.results[0].value;

expect(planingConstraintsURL).toBeNull();
expect(planningConstraintsResponse).toEqual({ data: null });
});
});

describe("demo state", () => {
beforeEach(() => {
act(() =>
Expand Down
Loading
Loading