diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
index fed2d67c66..0c0c150221 100644
--- a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
+++ b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
@@ -552,7 +552,7 @@ describe("Form validation and error handling", () => {
test(
"an error displays if the maximum number of items is exceeded",
- { timeout: 20000 },
+ { timeout: 25000 },
async () => {
const { user, getAllByTestId, getByTestId, getByText } = setup(
,
diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx
index 75b6aabbdf..dd6da0de94 100644
--- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx
+++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx
@@ -1,14 +1,11 @@
import { useSchema } from "@planx/components/shared/Schema/hook";
-import {
- Schema,
- SchemaUserData,
- SchemaUserResponse,
-} from "@planx/components/shared/Schema/model";
+import { Schema, SchemaUserData } from "@planx/components/shared/Schema/model";
import {
getPreviouslySubmittedData,
makeData,
} from "@planx/components/shared/utils";
import { FormikProps, useFormik } from "formik";
+import { get } from "lodash";
import React, {
createContext,
PropsWithChildren,
@@ -21,14 +18,13 @@ import { PresentationalProps } from ".";
interface MapAndLabelContextValue {
schema: Schema;
activeIndex: number;
- saveItem: () => Promise;
- editItem: (index: number) => void;
- cancelEditItem: () => void;
+ editFeature: (index: number) => void;
formik: FormikProps;
validateAndSubmitForm: () => void;
+ isFeatureInvalid: (index: number) => boolean;
+ addFeature: () => void;
mapAndLabelProps: PresentationalProps;
errors: {
- unsavedItem: boolean;
min: boolean;
max: boolean;
};
@@ -44,13 +40,15 @@ export const MapAndLabelProvider: React.FC = (
props,
) => {
const { schema, children, handleSubmit } = props;
- const { formikConfig, initialValues: _initialValues } = useSchema({
+ const { formikConfig, initialValues } = useSchema({
schema,
previousValues: getPreviouslySubmittedData(props),
});
const formik = useFormik({
...formikConfig,
+ // The user interactions are map driven - start with no values added
+ initialValues: { schemaData: [] },
onSubmit: (values) => {
const defaultPassportData = makeData(props, values.schemaData)?.["data"];
@@ -66,35 +64,16 @@ export const MapAndLabelProvider: React.FC = (
props.previouslySubmittedData ? -1 : 0,
);
- const [activeItemInitialState, setActiveItemInitialState] = useState<
- SchemaUserResponse | undefined
- >(undefined);
-
- const [unsavedItemError, setUnsavedItemError] = useState(false);
const [minError, setMinError] = useState(false);
const [maxError, setMaxError] = useState(false);
const resetErrors = () => {
setMinError(false);
setMaxError(false);
- setUnsavedItemError(false);
- };
-
- const saveItem = async () => {
- resetErrors();
-
- const errors = await formik.validateForm();
- const isValid = !errors.schemaData?.length;
- if (isValid) {
- exitEditMode();
- }
};
const validateAndSubmitForm = () => {
- // Do not allow submissions with an unsaved item
- if (activeIndex !== -1) return setUnsavedItemError(true);
-
- // Manually validate minimum number of items
+ // Manually validate minimum number of features
if (formik.values.schemaData.length < schema.min) {
return setMinError(true);
}
@@ -102,37 +81,38 @@ export const MapAndLabelProvider: React.FC = (
formik.handleSubmit();
};
- const cancelEditItem = () => {
- if (activeItemInitialState) resetItemToPreviousState();
-
- setActiveItemInitialState(undefined);
-
- exitEditMode();
- };
-
- const editItem = (index: number) => {
- setActiveItemInitialState(formik.values.schemaData[index]);
+ const editFeature = (index: number) => {
setActiveIndex(index);
};
- const exitEditMode = () => setActiveIndex(-1);
+ const isFeatureInvalid = (index: number) =>
+ Boolean(get(formik.errors, ["schemaData", index]));
+
+ const addFeature = () => {
+ resetErrors();
+
+ const currentFeatures = formik.values.schemaData;
+ const updatedFeatures = [...currentFeatures, initialValues];
+ formik.setFieldValue("schemaData", updatedFeatures);
- const resetItemToPreviousState = () =>
- formik.setFieldValue(`schemaData[${activeIndex}]`, activeItemInitialState);
+ // TODO: Handle more gracefully - stop user from adding new feature to map?
+ if (schema.max && updatedFeatures.length > schema.max) {
+ setMaxError(true);
+ }
+ };
return (
= ({
features,
}) => {
- const { schema, activeIndex, formik } = useMapAndLabelContext();
- const [activeTab, setActiveTab] = useState(
- features[features.length - 1].properties?.label || "",
- );
-
- const handleChange = (_event: React.SyntheticEvent, newValue: string) => {
- setActiveTab(newValue);
- };
+ const { schema, activeIndex, formik, editFeature, isFeatureInvalid } =
+ useMapAndLabelContext();
return (
= ({
maxHeight: "fit-content",
}}
>
-
+
{
+ editFeature(parseInt(newValue, 10));
+ }}
+ // TODO!
aria-label="Vertical tabs example"
sx={{ borderRight: 1, borderColor: "divider" }}
>
{features.map((feature, i) => (
({
+ backgroundColor: theme.palette.action.focus,
+ }),
+ })}
/>
))}
{features.map((feature, i) => (
= ({
} m²)`}
-
-
-
- console.log(`TODO - Copy data from another tab`)
- }
- name={""}
- style={{ width: "200px" }}
- >
- {features
- .filter(
- (feature) => feature.properties?.label !== activeTab,
- )
- .map((option) => (
-
- ))}
-
-
-
+ {features.length > 1 && (
+
+
+
+ console.log(`TODO - Copy data from another tab`)
+ }
+ name={""}
+ style={{ width: "200px" }}
+ >
+ {/* Iterate over all other features */}
+ {features
+ .filter((_, j) => j !== i)
+ .map((option) => (
+
+ ))}
+
+
+
+ )}
({
@@ -171,7 +175,8 @@ const VerticalFeatureTabs: React.FC<{ features: Feature[] }> = ({
};
const Root = () => {
- const { validateAndSubmitForm, mapAndLabelProps } = useMapAndLabelContext();
+ const { validateAndSubmitForm, mapAndLabelProps, errors } =
+ useMapAndLabelContext();
const {
title,
description,
@@ -188,11 +193,13 @@ const Root = () => {
} = mapAndLabelProps;
const [features, setFeatures] = useState(undefined);
+ const { addFeature, schema } = useMapAndLabelContext();
useEffect(() => {
const geojsonChangeHandler = ({ detail: geojson }: any) => {
if (geojson["EPSG:3857"]?.features) {
setFeatures(geojson["EPSG:3857"].features);
+ addFeature();
} else {
// if the user clicks 'reset' on the map, geojson will be empty object, so set features to undefined
setFeatures(undefined);
@@ -206,7 +213,12 @@ const Root = () => {
return function cleanup() {
map?.removeEventListener("geojsonChange", geojsonChangeHandler);
};
- }, [setFeatures]);
+ }, [setFeatures, addFeature]);
+
+ const rootError: string =
+ (errors.min && `You must provide at least ${schema.min} response(s)`) ||
+ (errors.max && `You can provide at most ${schema.max} response(s)`) ||
+ "";
return (
@@ -218,34 +230,36 @@ const Root = () => {
howMeasured={howMeasured}
/>
-
- {/* @ts-ignore */}
-
-
+
+
+ {/* @ts-ignore */}
+
+
+
{features && features?.length > 0 ? (
) : (