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: ChecklistFieldInput for List component #3304

Merged
merged 12 commits into from
Jul 1, 2024
Merged
74 changes: 22 additions & 52 deletions editor.planx.uk/src/@planx/components/Checklist/Public.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import { visuallyHidden } from "@mui/utils";
import type { Checklist, Group } from "@planx/components/Checklist/model";
import {
type Checklist,
checklistValidationSchema,
getFlatOptions,
type Group,
} from "@planx/components/Checklist/model";
import ImageButton from "@planx/components/shared/Buttons/ImageButton";
import Card from "@planx/components/shared/Preview/Card";
import CardHeader from "@planx/components/shared/Preview/CardHeader";
Expand All @@ -12,7 +17,7 @@ import FormWrapper from "ui/public/FormWrapper";
import FullWidthWrapper from "ui/public/FullWidthWrapper";
import ChecklistItem from "ui/shared/ChecklistItem";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import { array, object } from "yup";
import { object } from "yup";

import { Option } from "../shared";
import type { PublicProps } from "../ui";
Expand All @@ -31,36 +36,21 @@ function toggleInArray<T>(value: T, arr: Array<T>): Array<T> {
: [...arr, value];
}

function getFlatOptions({
options,
groupedOptions,
}: {
options: Checklist["options"];
groupedOptions: Checklist["groupedOptions"];
}) {
if (options) {
return options;
}
if (groupedOptions) {
return groupedOptions.flatMap((group) => group.children);
}
return [];
}
const ChecklistComponent: React.FC<Props> = (props) => {
const {
description = "",
groupedOptions,
handleSubmit,
howMeasured,
info,
options,
policyRef,
text,
img,
previouslySubmittedData,
id,
} = props;

const ChecklistComponent: React.FC<Props> = ({
allRequired,
description = "",
groupedOptions,
handleSubmit,
howMeasured,
info,
options,
policyRef,
text,
img,
previouslySubmittedData,
id,
}) => {
const formik = useFormik<{ checked: Array<string> }>({
initialValues: {
checked: previouslySubmittedData?.answers || [],
Expand All @@ -71,27 +61,7 @@ const ChecklistComponent: React.FC<Props> = ({
validateOnBlur: false,
validateOnChange: false,
validationSchema: object({
checked: array()
.required()
.test({
name: "atLeastOneChecked",
message: "Select at least one option",
test: (checked?: Array<string>) => {
return Boolean(checked && checked.length > 0);
},
})
.test({
name: "notAllChecked",
message: "All options must be checked",
test: (checked?: Array<string>) => {
if (!allRequired) {
return true;
}
const flatOptions = getFlatOptions({ options, groupedOptions });
const allChecked = checked && checked.length === flatOptions.length;
return Boolean(allChecked);
},
}),
checked: checklistValidationSchema(props),
}),
});

Expand Down
45 changes: 45 additions & 0 deletions editor.planx.uk/src/@planx/components/Checklist/model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { array } from "yup";

import { MoreInformation, Option } from "../shared";

export interface Group<T> {
Expand Down Expand Up @@ -56,3 +58,46 @@ export const toggleExpandableChecklist = (
};
}
};

export const getFlatOptions = ({
options,
groupedOptions,
}: {
options: Checklist["options"];
groupedOptions: Checklist["groupedOptions"];
}) => {
if (options) {
return options;
}
if (groupedOptions) {
return groupedOptions.flatMap((group) => group.children);
}
return [];
};

export const checklistValidationSchema = ({
allRequired,
options,
groupedOptions,
}: Checklist) =>
array()
.required()
.test({
name: "atLeastOneChecked",
message: "Select at least one option",
test: (checked?: Array<string>) => {
return Boolean(checked && checked.length > 0);
},
})
.test({
name: "notAllChecked",
message: "All options must be checked",
test: (checked?: Array<string>) => {
if (!allRequired) {
return true;
}
const flatOptions = getFlatOptions({ options, groupedOptions });
const allChecked = checked && checked.length === flatOptions.length;
return Boolean(allChecked);
},
});
16 changes: 15 additions & 1 deletion editor.planx.uk/src/@planx/components/List/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import { CommunalSpaceGLA } from "./schemas/GLA/CommunalSpace";
import { ExistingAndProposedUsesGLA } from "./schemas/GLA/ExistingAndProposedUses";
import { OpenSpaceGLA } from "./schemas/GLA/OpenSpace";
import { ProtectedSpaceGLA } from "./schemas/GLA/ProtectedSpace";
import { Zoo } from "./schemas/mocks/Zoo";
import { ResidentialUnitsExisting } from "./schemas/ResidentialUnits/Existing";
import { ResidentialUnitsGLAGained } from "./schemas/ResidentialUnits/GLA/Gained";
import { ResidentialUnitsGLALost } from "./schemas/ResidentialUnits/GLA/Lost";
import { ResidentialUnitsGLANew } from "./schemas/ResidentialUnits/GLA/New";
import { ResidentialUnitsGLARebuilt } from "./schemas/ResidentialUnits/GLA/Rebuilt";
import { ResidentialUnitsGLARemoved } from "./schemas/ResidentialUnits/GLA/Removed";
import { ResidentialUnitsProposed } from "./schemas/ResidentialUnits/Proposed";

type Props = EditorProps<TYPES.List, List>;
Expand All @@ -36,6 +38,18 @@ export const SCHEMAS = [
schema: ResidentialUnitsGLAGained,
},
{ name: "Residential units (GLA) - Lost", schema: ResidentialUnitsGLALost },
{
name: "Residential units (GLA) - New",
schema: ResidentialUnitsGLANew,
},
{
name: "Residential units (GLA) - Rebuilt",
schema: ResidentialUnitsGLARebuilt,
},
{
name: "Residential units (GLA) - Removed",
schema: ResidentialUnitsGLARemoved,
},
{ name: "Non-residential floorspace", schema: NonResidentialFloorspace },
{
name: "Existing and proposed uses (GLA)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
const defaultPassportData = makeData(props, values.userData)?.["data"];

// flattenedPassportData makes individual list items compatible with Calculate components
const flattenedPassportData = flatten(defaultPassportData);
const flattenedPassportData = flatten(defaultPassportData, { depth: 2 });
DafyddLlyr marked this conversation as resolved.
Show resolved Hide resolved

// basic example of general summary stats we can add onSubmit:
// 1. count of items/responses
Expand Down
64 changes: 62 additions & 2 deletions editor.planx.uk/src/@planx/components/List/Public/Fields.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import Grid from "@mui/material/Grid";
import MenuItem from "@mui/material/MenuItem";
import RadioGroup from "@mui/material/RadioGroup";
import { Option } from "@planx/components/shared";
import { visuallyHidden } from "@mui/utils";
import { getIn } from "formik";
import React from "react";
import SelectInput from "ui/editor/SelectInput";
import InputLabel from "ui/public/InputLabel";
import ChecklistItem from "ui/shared/ChecklistItem";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input";
import InputRowLabel from "ui/shared/InputRowLabel";

import { DESCRIPTION_TEXT, ERROR_MESSAGE } from "../../shared/constants";
import BasicRadio from "../../shared/Radio/BasicRadio";
import type { NumberField, QuestionField, TextField } from "../model";
import type {
ChecklistField,
NumberField,
QuestionField,
TextField,
} from "../model";
import { useListContext } from "./Context";
import { get } from "lodash";

Expand Down Expand Up @@ -183,3 +191,55 @@ export const SelectFieldInput: React.FC<Props<QuestionField>> = (props) => {
</InputLabel>
);
};

export const ChecklistFieldInput: React.FC<Props<ChecklistField>> = (props) => {
const { formik, activeIndex } = useListContext();
const {
id,
data: { options, title, fn },
} = props;

const changeCheckbox =
(id: string) =>
async (
_checked: React.MouseEvent<HTMLButtonElement, MouseEvent> | undefined,
) => {
let newCheckedIds;

if (formik.values.userData[activeIndex][fn].includes(id)) {
newCheckedIds = (
formik.values.userData[activeIndex][fn] as string[]
).filter((x) => x !== id);
} else {
newCheckedIds = [...formik.values.userData[activeIndex][fn], id];
}

await formik.setFieldValue(
`userData[${activeIndex}]['${fn}']`,
newCheckedIds,
);
};

return (
<InputLabel label={title} id={`checklist-label-${id}`}>
<ErrorWrapper
error={getIn(formik.errors, `userData[${activeIndex}]['${fn}']`)}
id={id}
>
<Grid container component="fieldset">
<legend style={visuallyHidden}>{title}</legend>
{options.map((option) => (
<ChecklistItem
onChange={changeCheckbox(option.id)}
label={option.data.text}
id={option.id}
checked={formik.values.userData[activeIndex][fn].includes(
option.id,
)}
/>
))}
</Grid>
</ErrorWrapper>
</InputLabel>
);
};
15 changes: 14 additions & 1 deletion editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,15 @@ describe("Form validation and error handling", () => {
expect(cuteInputErrorMessage).toHaveTextContent(/Select your answer before continuing/);
});

test.todo("checklist fields")
test("checklist fields", async () => {
const { user, getByRole, getByTestId } = setup(<ListComponent {...mockZooProps} />);

await user.click(getByRole("button", { name: /Save/ }));

const foodInputErrorMessage = getByTestId(/error-message-input-checklist-food/);

expect(foodInputErrorMessage).toHaveTextContent(/Select at least one option/);
})
});

test("an error displays if the minimum number of items is not met", async () => {
Expand Down Expand Up @@ -651,6 +659,11 @@ const fillInResponse = async (user: UserEvent) => {
const cuteRadio = screen.getAllByRole("radio")[0];
await user.click(cuteRadio);

const eatCheckboxes = screen.getAllByRole("checkbox");
await user.click(eatCheckboxes[0]);
await user.click(eatCheckboxes[1]);
await user.click(eatCheckboxes[2]);

const saveButton = screen.getByRole("button", {
name: /Save/,
});
Expand Down
5 changes: 4 additions & 1 deletion editor.planx.uk/src/@planx/components/List/Public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { Field, List } from "../model";
import { formatSchemaDisplayValue } from "../utils";
import { ListProvider, useListContext } from "./Context";
import {
ChecklistFieldInput,
NumberFieldInput,
RadioFieldInput,
SelectFieldInput,
Expand Down Expand Up @@ -59,6 +60,8 @@ const InputField: React.FC<Field> = (props) => {
return <RadioFieldInput id={inputFieldId} {...props} />;
}
return <SelectFieldInput id={inputFieldId} {...props} />;
case "checklist":
return <ChecklistFieldInput id={inputFieldId} {...props} />;
}
};

Expand Down Expand Up @@ -126,7 +129,7 @@ const InactiveListCard: React.FC<{
<TableCell>
{formatSchemaDisplayValue(
formik.values.userData[i][field.data.fn],
schema,
schema.fields[j],
)}
</TableCell>
</TableRow>
Expand Down
Loading
Loading