Skip to content

Commit

Permalink
feat: add a "map" field to the List component (#3505)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessicamcinchak authored Aug 19, 2024
1 parent e0bb2a9 commit aaba113
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 10 deletions.
5 changes: 4 additions & 1 deletion editor.planx.uk/src/@planx/components/List/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import MenuItem from "@mui/material/MenuItem";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { useFormik } from "formik";
import { hasFeatureFlag } from "lib/featureFlags";
import React from "react";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
Expand All @@ -27,6 +28,7 @@ import { ResidentialUnitsGLARebuilt } from "./schemas/ResidentialUnits/GLA/Rebui
import { ResidentialUnitsGLARemoved } from "./schemas/ResidentialUnits/GLA/Removed";
import { ResidentialUnitsGLARetained } from "./schemas/ResidentialUnits/GLA/Retained";
import { ResidentialUnitsProposed } from "./schemas/ResidentialUnits/Proposed";
import { Trees } from "./schemas/Trees";

type Props = EditorProps<TYPES.List, List>;

Expand Down Expand Up @@ -60,7 +62,8 @@ export const SCHEMAS = [
{ name: "Protected spaces (GLA)", schema: ProtectedSpaceGLA },
{ name: "Open spaces (GLA)", schema: OpenSpaceGLA },
{ name: "Proposed advertisements", schema: ProposedAdvertisements },
] as const;
...(hasFeatureFlag("TREES") ? [{ name: "Trees", schema: Trees }] : []),
];

function ListComponent(props: Props) {
const formik = useFormik({
Expand Down
90 changes: 84 additions & 6 deletions editor.planx.uk/src/@planx/components/List/Public/Fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import MenuItem from "@mui/material/MenuItem";
import RadioGroup from "@mui/material/RadioGroup";
import { visuallyHidden } from "@mui/utils";
import { paddedDate } from "@planx/components/DateInput/model";
import { MapContainer } from "@planx/components/shared/Preview/MapContainer";
import { getIn } from "formik";
import { Feature } from "geojson";
import { get } from "lodash";
import React from "react";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useState } from "react";
import SelectInput from "ui/editor/SelectInput";
import InputLabel from "ui/public/InputLabel";
import ChecklistItem from "ui/shared/ChecklistItem";
Expand All @@ -22,6 +25,7 @@ import BasicRadio from "../../shared/Radio/BasicRadio";
import type {
ChecklistField,
DateField,
MapField,
NumberField,
QuestionField,
TextField,
Expand Down Expand Up @@ -230,10 +234,7 @@ export const ChecklistFieldInput: React.FC<Props<ChecklistField>> = (props) => {
);
};

export const DateFieldInput: React.FC<Props<DateField>> = ({
id,
data,
}) => {
export const DateFieldInput: React.FC<Props<DateField>> = ({ id, data }) => {
const { formik, activeIndex } = useListContext();

return (
Expand All @@ -243,7 +244,10 @@ export const DateFieldInput: React.FC<Props<DateField>> = ({
value={formik.values.userData[activeIndex][data.fn] as string}
bordered
onChange={(newDate: string, eventType: string) => {
formik.setFieldValue(`userData[${activeIndex}]['${data.fn}']`, paddedDate(newDate, eventType));
formik.setFieldValue(
`userData[${activeIndex}]['${data.fn}']`,
paddedDate(newDate, eventType),
);
}}
error={get(formik.errors, ["userData", activeIndex, data.fn])}
id={id}
Expand All @@ -252,3 +256,77 @@ export const DateFieldInput: React.FC<Props<DateField>> = ({
</InputLabel>
);
};

export const MapFieldInput: React.FC<Props<MapField>> = (props) => {
const { formik, activeIndex, schema } = useListContext();
const {
id,
data: { title, fn, mapOptions },
} = props;

const teamSettings = useStore.getState().teamSettings;
const passport = useStore((state) => state.computePassport());

const [_features, setFeatures] = useState<Feature[] | undefined>(undefined);

useEffect(() => {
const geojsonChangeHandler = async ({ detail: geojson }: any) => {
if (geojson["EPSG:3857"]?.features) {
setFeatures(geojson["EPSG:3857"].features);
await formik.setFieldValue(
`userData[${activeIndex}]['${fn}']`,
geojson["EPSG:3857"].features,
);
} else {
// if the user clicks 'reset' on the map, geojson will be empty object, so set features to undefined
setFeatures(undefined);
await formik.setFieldValue(
`userData[${activeIndex}]['${fn}']`,
undefined,
);
}
};

const map: any = document.getElementById(id);

map?.addEventListener("geojsonChange", geojsonChangeHandler);

return function cleanup() {
map?.removeEventListener("geojsonChange", geojsonChangeHandler);
};
}, [setFeatures]);

return (
<InputLabel label={title} id={`map-label-${id}`} htmlFor={id}>
<ErrorWrapper
error={getIn(formik.errors, `userData[${activeIndex}]['${fn}']`)}
id={id}
>
<MapContainer environment="standalone">
{/* @ts-ignore */}
<my-map
id={id}
ariaLabelOlFixedOverlay={`An interactive map for plotting and describing ${schema.type.toLocaleLowerCase()}`}
height={400}
basemap={mapOptions?.basemap}
drawMode
drawMany={mapOptions?.drawMany}
drawColor={mapOptions?.drawColor}
drawType={mapOptions?.drawType}
drawPointer="crosshair"
zoom={20}
maxZoom={23}
latitude={Number(passport?.data?._address?.latitude)}
longitude={Number(passport?.data?._address?.longitude)}
osProxyEndpoint={`${process.env.REACT_APP_API_URL}/proxy/ordnance-survey`}
osCopyright={`Basemap subject to Crown copyright and database rights ${new Date().getFullYear()} OS (0)100024857`}
clipGeojsonData={
teamSettings?.boundaryBBox &&
JSON.stringify(teamSettings?.boundaryBBox)
}
/>
</MapContainer>
</ErrorWrapper>
</InputLabel>
);
};
3 changes: 3 additions & 0 deletions editor.planx.uk/src/@planx/components/List/Public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { formatSchemaDisplayValue } from "../utils";
import { ListProvider, useListContext } from "./Context";
import {
ChecklistFieldInput,
MapFieldInput,
DateFieldInput,
NumberFieldInput,
RadioFieldInput,
Expand Down Expand Up @@ -65,6 +66,8 @@ const InputField: React.FC<Field> = (props) => {
return <ChecklistFieldInput id={inputFieldId} {...props} />;
case "date":
return <DateFieldInput id={inputFieldId} {...props} />;
case "map":
return <MapFieldInput id={inputFieldId} {...props} />;
}
};

Expand Down
43 changes: 40 additions & 3 deletions editor.planx.uk/src/@planx/components/List/model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Feature } from "geojson";
import { cloneDeep } from "lodash";
import { array, BaseSchema, object, ObjectSchema, string } from "yup";

Expand Down Expand Up @@ -65,11 +66,31 @@ export type DateField = {
data: DateInput & { fn: string };
};

export type MapField = {
type: "map";
data: {
title: string;
fn: string;
mapOptions?: {
basemap?: "OSVectorTile" | "OSRaster" | "MapboxSatellite" | "OSM";
drawType?: "Point" | "Polygon";
drawColor?: string;
drawMany?: boolean;
};
};
};

/**
* Represents the input types available in the List component
* Existing models are used to allow to us to re-use existing components, maintaining consistend UX/UI
*/
export type Field = TextField | NumberField | QuestionField | ChecklistField | DateField;
export type Field =
| TextField
| NumberField
| QuestionField
| ChecklistField
| DateField
| MapField;

/**
* Models the form displayed to the user
Expand All @@ -81,7 +102,7 @@ export interface Schema {
max?: number;
}

export type UserResponse = Record<Field["data"]["fn"], string | string[]>;
export type UserResponse = Record<Field["data"]["fn"], string | any[]>; // string | string[] | Feature[]

export type UserData = { userData: UserResponse[] };

Expand All @@ -102,6 +123,19 @@ export const parseContent = (data: Record<string, any> | undefined): List => ({
...parseMoreInformation(data),
});

const mapValidationSchema = ({ mapOptions }: MapField["data"]) =>
array()
.required()
.test({
name: "atLeastOneFeature",
message: `Draw at least one ${
mapOptions?.drawType?.toLocaleLowerCase() || "feature"
} on the map`,
test: (features?: Array<Feature>) => {
return Boolean(features && features?.length > 0);
},
});

/**
* For each field in schema, return a map of Yup validation schema
* Matches both the field type and data
Expand All @@ -128,6 +162,9 @@ const generateValidationSchemaForFields = (
case "date":
fieldSchemas[data.fn] = dateValidationSchema(data);
break;
case "map":
fieldSchemas[data.fn] = mapValidationSchema(data);
break;
}
});

Expand All @@ -154,7 +191,7 @@ export const generateValidationSchema = (schema: Schema) => {
export const generateInitialValues = (schema: Schema): UserResponse => {
const initialValues: UserResponse = {};
schema.fields.forEach((field) => {
field.type === "checklist"
["checklist", "map"].includes(field.type)
? (initialValues[field.data.fn] = [])
: (initialValues[field.data.fn] = "");
});
Expand Down
78 changes: 78 additions & 0 deletions editor.planx.uk/src/@planx/components/List/schemas/Trees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Schema } from "@planx/components/List/model";
import { TextInputType } from "@planx/components/TextInput/model";

export const Trees: Schema = {
type: "Tree",
fields: [
{
type: "text",
data: {
title: "Species",
fn: "species",
type: TextInputType.Short,
},
},
{
type: "text",
data: {
title: "Proposed work",
fn: "work",
type: TextInputType.Short,
},
},
{
type: "text",
data: {
title: "Justification",
fn: "justification",
type: TextInputType.Short,
},
},
{
type: "question",
data: {
title: "Urgency",
fn: "urgency",
options: [
{
id: "low",
data: { text: "Low", val: "low" },
},
{
id: "moderate",
data: { text: "Moderate", val: "moderate" },
},
{
id: "high",
data: { text: "High", val: "high" },
},
{
id: "urgent",
data: { text: "Urgent", val: "urgent" },
},
],
},
},
{
type: "date",
data: {
title: "Expected completion date",
fn: "completionDate",
},
},
{
type: "map",
data: {
title: "Where is it?",
fn: "features",
mapOptions: {
basemap: "OSVectorTile",
drawType: "Point",
drawColor: "#ff0000",
drawMany: true,
},
},
},
],
min: 1,
} as const;

0 comments on commit aaba113

Please sign in to comment.