diff --git a/editor.planx.uk/src/@planx/components/Checklist/Public.tsx b/editor.planx.uk/src/@planx/components/Checklist/Public.tsx index 768bc86f73..6383d8abcd 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Checklist/Public.tsx @@ -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"; @@ -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"; @@ -31,36 +36,21 @@ function toggleInArray(value: T, arr: Array): Array { : [...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) => { + const { + description = "", + groupedOptions, + handleSubmit, + howMeasured, + info, + options, + policyRef, + text, + img, + previouslySubmittedData, + id, + } = props; -const ChecklistComponent: React.FC = ({ - allRequired, - description = "", - groupedOptions, - handleSubmit, - howMeasured, - info, - options, - policyRef, - text, - img, - previouslySubmittedData, - id, -}) => { const formik = useFormik<{ checked: Array }>({ initialValues: { checked: previouslySubmittedData?.answers || [], @@ -71,27 +61,7 @@ const ChecklistComponent: React.FC = ({ validateOnBlur: false, validateOnChange: false, validationSchema: object({ - checked: array() - .required() - .test({ - name: "atLeastOneChecked", - message: "Select at least one option", - test: (checked?: Array) => { - return Boolean(checked && checked.length > 0); - }, - }) - .test({ - name: "notAllChecked", - message: "All options must be checked", - test: (checked?: Array) => { - if (!allRequired) { - return true; - } - const flatOptions = getFlatOptions({ options, groupedOptions }); - const allChecked = checked && checked.length === flatOptions.length; - return Boolean(allChecked); - }, - }), + checked: checklistValidationSchema(props), }), }); diff --git a/editor.planx.uk/src/@planx/components/Checklist/model.ts b/editor.planx.uk/src/@planx/components/Checklist/model.ts index 843be32901..7287fe9364 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/model.ts +++ b/editor.planx.uk/src/@planx/components/Checklist/model.ts @@ -1,3 +1,5 @@ +import { array } from "yup"; + import { MoreInformation, Option } from "../shared"; export interface Group { @@ -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) => { + return Boolean(checked && checked.length > 0); + }, + }) + .test({ + name: "notAllChecked", + message: "All options must be checked", + test: (checked?: Array) => { + if (!allRequired) { + return true; + } + const flatOptions = getFlatOptions({ options, groupedOptions }); + const allChecked = checked && checked.length === flatOptions.length; + return Boolean(allChecked); + }, + }); diff --git a/editor.planx.uk/src/@planx/components/List/Editor.tsx b/editor.planx.uk/src/@planx/components/List/Editor.tsx index 6b85d49b87..bfa3e99ae8 100644 --- a/editor.planx.uk/src/@planx/components/List/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/List/Editor.tsx @@ -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; @@ -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)", diff --git a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx index bca7c02f9e..38bb5b1ec4 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -171,7 +171,7 @@ export const ListProvider: React.FC = (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 }); // basic example of general summary stats we can add onSubmit: // 1. count of items/responses diff --git a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx index a5313d3cfb..8fad6c35db 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx @@ -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"; @@ -183,3 +191,55 @@ export const SelectFieldInput: React.FC> = (props) => { ); }; + +export const ChecklistFieldInput: React.FC> = (props) => { + const { formik, activeIndex } = useListContext(); + const { + id, + data: { options, title, fn }, + } = props; + + const changeCheckbox = + (id: string) => + async ( + _checked: React.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 ( + + + + {title} + {options.map((option) => ( + + ))} + + + + ); +}; 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 5bdaa44f8b..b2c2d6ec73 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 @@ -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(); + + 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 () => { @@ -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/, }); diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.tsx index 66efdcd9d9..c0d3136c29 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -20,6 +20,7 @@ import type { Field, List } from "../model"; import { formatSchemaDisplayValue } from "../utils"; import { ListProvider, useListContext } from "./Context"; import { + ChecklistFieldInput, NumberFieldInput, RadioFieldInput, SelectFieldInput, @@ -59,6 +60,8 @@ const InputField: React.FC = (props) => { return ; } return ; + case "checklist": + return ; } }; @@ -126,7 +129,7 @@ const InactiveListCard: React.FC<{ {formatSchemaDisplayValue( formik.values.userData[i][field.data.fn], - schema, + schema.fields[j], )} diff --git a/editor.planx.uk/src/@planx/components/List/model.ts b/editor.planx.uk/src/@planx/components/List/model.ts index 65adc60975..759aae527b 100644 --- a/editor.planx.uk/src/@planx/components/List/model.ts +++ b/editor.planx.uk/src/@planx/components/List/model.ts @@ -1,6 +1,7 @@ import { cloneDeep } from "lodash"; import { array, BaseSchema, object, ObjectSchema, string } from "yup"; +import { checklistValidationSchema } from "../Checklist/model"; import { NumberInput, numberInputValidationSchema } from "../NumberInput/model"; import { MoreInformation, Option, parseMoreInformation } from "../shared"; import { @@ -20,6 +21,12 @@ interface QuestionInput { options: Option[]; } +interface ChecklistInput { + title: string; + description?: string; + options: Option[]; +} + /** * As above, we need a simplified validation schema for QuestionsInputs */ @@ -42,12 +49,17 @@ export type QuestionField = { type: "question"; data: QuestionInput & { fn: string }; }; +export type ChecklistField = { + type: "checklist"; + required?: true; + data: ChecklistInput & { fn: string }; +}; /** * 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; +export type Field = TextField | NumberField | QuestionField | ChecklistField; /** * Models the form displayed to the user @@ -59,7 +71,7 @@ export interface Schema { max?: number; } -export type UserResponse = Record; +export type UserResponse = Record; export type UserData = { userData: UserResponse[] }; @@ -100,6 +112,9 @@ const generateValidationSchemaForFields = ( case "question": fieldSchemas[data.fn] = questionInputValidationSchema(data); break; + case "checklist": + fieldSchemas[data.fn] = checklistValidationSchema(data); + break; } }); @@ -125,6 +140,10 @@ export const generateValidationSchema = (schema: Schema) => { export const generateInitialValues = (schema: Schema): UserResponse => { const initialValues: UserResponse = {}; - schema.fields.forEach((field) => (initialValues[field.data.fn] = "")); + schema.fields.forEach((field) => { + field.type === "checklist" + ? (initialValues[field.data.fn] = []) + : (initialValues[field.data.fn] = ""); + }); return initialValues; }; diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Gained.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Gained.ts index b08d3bbc6f..f403091c5c 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Gained.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Gained.ts @@ -70,61 +70,25 @@ export const ResidentialUnitsGLAGained: Schema = { ], }, }, - // { - // type: "checklist", // @todo - // data: { - // title: "Is this unit compliant with any of the following?", - // fn: "compliance", - // options: [ - // { - // id: "m42", - // data: { text: "Part M4(2) of the Building Regulations 2010" }, - // }, - // { - // id: "m432a", - // data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, - // }, - // { - // id: "m432b", - // data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, - // }, - // { id: "none", data: { text: "None of these" } }, - // ], - // }, - // }, { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", - fn: "compliance.m42", - options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, - ], - }, - }, - { - type: "question", + type: "checklist", data: { - title: - "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", - fn: "compliance.m432a", + title: "Is this unit compliant with any of the following?", + fn: "compliance", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, - ], - }, - }, - { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", - fn: "compliance.m432b", - options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { + id: "m42", + data: { text: "Part M4(2) of the Building Regulations 2010" }, + }, + { + id: "m432a", + data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, + }, + { + id: "m432b", + data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, + }, + { id: "none", data: { text: "None of these" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Lost.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Lost.ts index 5c4d126916..bed690a2e7 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Lost.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Lost.ts @@ -53,61 +53,25 @@ export const ResidentialUnitsGLALost: Schema = { ], }, }, - // { - // type: "checklist", // @todo - // data: { - // title: "Is this unit compliant with any of the following?", - // fn: "compliance", - // options: [ - // { - // id: "m42", - // data: { text: "Part M4(2) of the Building Regulations 2010" }, - // }, - // { - // id: "m432a", - // data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, - // }, - // { - // id: "m432b", - // data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, - // }, - // { id: "none", data: { text: "None of these" } }, - // ], - // }, - // }, { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", - fn: "compliance.m42", - options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, - ], - }, - }, - { - type: "question", + type: "checklist", data: { - title: - "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", - fn: "compliance.m432a", + title: "Is this unit compliant with any of the following?", + fn: "compliance", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, - ], - }, - }, - { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", - fn: "compliance.m432b", - options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { + id: "m42", + data: { text: "Part M4(2) of the Building Regulations 2010" }, + }, + { + id: "m432a", + data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, + }, + { + id: "m432b", + data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, + }, + { id: "none", data: { text: "None of these" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts index 67a67ff4d0..fe8f9a2a78 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts @@ -90,61 +90,25 @@ export const ResidentialUnitsGLANew: Schema = { ], }, }, - // { - // type: "checklist", // @todo - // data: { - // title: "Is this unit compliant with any of the following?", - // fn: "compliance", - // options: [ - // { - // id: "m42", - // data: { text: "Part M4(2) of the Building Regulations 2010" }, - // }, - // { - // id: "m432a", - // data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, - // }, - // { - // id: "m432b", - // data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, - // }, - // { id: "none", data: { text: "None of these" } }, - // ], - // }, - // }, { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", - fn: "compliance.m42", - options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, - ], - }, - }, - { - type: "question", + type: "checklist", data: { - title: - "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", - fn: "compliance.m432a", + title: "Is this unit compliant with any of the following?", + fn: "compliance", options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, - ], - }, - }, - { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", - fn: "compliance.m432b", - options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, + { + id: "m42", + data: { text: "Part M4(2) of the Building Regulations 2010" }, + }, + { + id: "m432a", + data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, + }, + { + id: "m432b", + data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, + }, + { id: "none", data: { text: "None of these" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts index 31953e66b6..b108866be2 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts @@ -90,61 +90,25 @@ export const ResidentialUnitsGLARebuilt: Schema = { ], }, }, - // { - // type: "checklist", // @todo - // data: { - // title: "Is this unit compliant with any of the following?", - // fn: "compliance", - // options: [ - // { - // id: "m42", - // data: { text: "Part M4(2) of the Building Regulations 2010" }, - // }, - // { - // id: "m432a", - // data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, - // }, - // { - // id: "m432b", - // data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, - // }, - // { id: "none", data: { text: "None of these" } }, - // ], - // }, - // }, { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", - fn: "compliance.m42", - options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, - ], - }, - }, - { - type: "question", + type: "checklist", data: { - title: - "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", - fn: "compliance.m432a", + title: "Is this unit compliant with any of the following?", + fn: "compliance", options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, - ], - }, - }, - { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", - fn: "compliance.m432b", - options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, + { + id: "m42", + data: { text: "Part M4(2) of the Building Regulations 2010" }, + }, + { + id: "m432a", + data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, + }, + { + id: "m432b", + data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, + }, + { id: "none", data: { text: "None of these" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts index 4c5d4ecd94..b5fde1a983 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts @@ -69,61 +69,25 @@ export const ResidentialUnitsGLARemoved: Schema = { ], }, }, - // { - // type: "checklist", // @todo - // data: { - // title: "Is this unit compliant with any of the following?", - // fn: "compliance", - // options: [ - // { - // id: "m42", - // data: { text: "Part M4(2) of the Building Regulations 2010" }, - // }, - // { - // id: "m432a", - // data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, - // }, - // { - // id: "m432b", - // data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, - // }, - // { id: "none", data: { text: "None of these" } }, - // ], - // }, - // }, { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", - fn: "compliance.m42", - options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, - ], - }, - }, - { - type: "question", + type: "checklist", data: { - title: - "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", - fn: "compliance.m432a", + title: "Is this unit compliant with any of the following?", + fn: "compliance", options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, - ], - }, - }, - { - type: "question", - data: { - title: - "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", - fn: "compliance.m432b", - options: [ - { id: "true", data: { text: "Yes", val: "true" } }, - { id: "false", data: { text: "No", val: "false" } }, + { + id: "m42", + data: { text: "Part M4(2) of the Building Regulations 2010" }, + }, + { + id: "m432a", + data: { text: "Part M4(3)(2a) of the Building Regulations 2010" }, + }, + { + id: "m432b", + data: { text: "Part M4(3)(2b) of the Building Regulations 2010" }, + }, + { id: "none", data: { text: "None of these" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/mocks/Zoo.ts b/editor.planx.uk/src/@planx/components/List/schemas/mocks/Zoo.ts index fe94d763e4..59394a6f31 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/mocks/Zoo.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/mocks/Zoo.ts @@ -3,10 +3,6 @@ import { TextInputType } from "@planx/components/TextInput/model"; import { Schema } from "../../model"; import { Props } from "../../Public"; -/** - * Temp simple example to build out UI - * Can be re-used as mock for testing - */ export const Zoo: Schema = { type: "Animal", fields: [ @@ -64,6 +60,20 @@ export const Zoo: Schema = { ], }, }, + // Checklist + { + type: "checklist", + data: { + title: "What do they eat?", + fn: "food", + options: [ + { id: "meat", data: { text: "Meat" } }, + { id: "leaves", data: { text: "Leaves" } }, + { id: "bamboo", data: { text: "Bamboo" } }, + { id: "fruit", data: { text: "fruit" } }, + ], + }, + }, ], min: 1, max: 3, @@ -86,6 +96,7 @@ export const mockZooPayload = { "email.address": "richard.parker@pi.com", name: "Richard Parker", size: "Medium", + food: ["meat", "leaves", "bamboo"], }, { age: 10, @@ -93,6 +104,7 @@ export const mockZooPayload = { "email.address": "richard.parker@pi.com", name: "Richard Parker", size: "Medium", + food: ["meat", "leaves", "bamboo"], }, ], "mockFn.one.age": 10, @@ -100,11 +112,13 @@ export const mockZooPayload = { "mockFn.one.email.address": "richard.parker@pi.com", "mockFn.one.name": "Richard Parker", "mockFn.one.size": "Medium", + "mockFn.one.food": ["meat", "leaves", "bamboo"], "mockFn.two.age": 10, "mockFn.two.cuteness.amount": "Very", "mockFn.two.email.address": "richard.parker@pi.com", "mockFn.two.name": "Richard Parker", "mockFn.two.size": "Medium", + "mockFn.two.food": ["meat", "leaves", "bamboo"], "mockFn.total.listItems": 2, }, }; diff --git a/editor.planx.uk/src/@planx/components/List/utils.test.ts b/editor.planx.uk/src/@planx/components/List/utils.test.ts index c34dbf20d8..72e8996e19 100644 --- a/editor.planx.uk/src/@planx/components/List/utils.test.ts +++ b/editor.planx.uk/src/@planx/components/List/utils.test.ts @@ -15,6 +15,7 @@ describe("passport data shape", () => { "email.address": "richard.parker@pi.com", name: "Richard Parker", size: "Medium", + food: ["bamboo", "leaves"], }, { age: 10, @@ -22,21 +23,24 @@ describe("passport data shape", () => { "email.address": "richard.parker@pi.com", name: "Richard Parker", size: "Medium", + food: ["meat", "bamboo", "leaves"], }, ], }; - expect(flatten(defaultPassportData)).toEqual({ + expect(flatten(defaultPassportData, { depth: 2 })).toEqual({ "mockFn.one.age": 10, "mockFn.one.cuteness.amount": "Very", "mockFn.one.email.address": "richard.parker@pi.com", "mockFn.one.name": "Richard Parker", "mockFn.one.size": "Medium", + "mockFn.one.food": ["bamboo", "leaves"], "mockFn.two.age": 10, "mockFn.two.cuteness.amount": "Very", "mockFn.two.email.address": "richard.parker@pi.com", "mockFn.two.name": "Richard Parker", "mockFn.two.size": "Medium", + "mockFn.two.food": ["meat", "bamboo", "leaves"], }); }); diff --git a/editor.planx.uk/src/@planx/components/List/utils.ts b/editor.planx.uk/src/@planx/components/List/utils.tsx similarity index 65% rename from editor.planx.uk/src/@planx/components/List/utils.ts rename to editor.planx.uk/src/@planx/components/List/utils.tsx index 0e0d0dcdc3..7b26eee627 100644 --- a/editor.planx.uk/src/@planx/components/List/utils.ts +++ b/editor.planx.uk/src/@planx/components/List/utils.tsx @@ -1,24 +1,47 @@ -import { QuestionField, Schema, UserResponse } from "./model"; +import React from "react"; + +import { Field, UserResponse } from "./model"; +import { styled } from "@mui/material/styles"; + +const List = styled("ul")(() => ({ + listStylePosition: "inside", + padding: 0, + margin: 0, +})) /** - * In the case of "question" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport + * In the case of "question" and "checklist" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport * @param value - the `val` or `text` of an Option defined in the schema's fields - * @param schema - the Schema object - * @returns string - the `text` for the given value `val`, or the original value + * @param field - the Field object + * @returns string | React.JSX.Element - the `text` for the given value `val`, or the original value */ -export function formatSchemaDisplayValue(value: string, schema: Schema) { - const questionFields = schema.fields.filter( - (field) => field.type === "question", - ) as QuestionField[]; - const matchingField = questionFields?.find((field) => - field.data.options.some((option) => option.data.val === value), - ); - const matchingOption = matchingField?.data.options.find( - (option) => option.data.val === value, - ); - - // If we found a "val" match, return its text, else just return the value as passed in - return matchingOption?.data?.text || value; +export function formatSchemaDisplayValue( + value: string | string[], + field: Field, +) { + switch (field.type) { + case "number": + case "text": + return value; + case "checklist": { + const matchingOptions = field.data.options.filter((option) => + (value as string[]).includes(option.id), + ); + return ( + + {matchingOptions.map((option) => ( +
  • {option.data.text}
  • + ))} +
    + ); + } + case "question": { + const matchingOption = field.data.options.find( + (option) => option.data.val === value, + ); + return matchingOption?.data.text; + } + } } /** @@ -32,7 +55,11 @@ export function sumIdenticalUnits( passportData: Record, ): number { let sum = 0; - passportData[`${fn}`].map((item) => (sum += parseInt(item?.identicalUnits))); + passportData[`${fn}`].map((item) => { + if (!Array.isArray(item?.identicalUnits)) { + sum += parseInt(item?.identicalUnits); + } + }); return sum; } @@ -58,10 +85,11 @@ export function sumIdenticalUnitsByDevelopmentType( newBuild: 0, notKnown: 0, }; - passportData[`${fn}`].map( - (item) => - (baseSums[`${item?.development}`] += parseInt(item?.identicalUnits)), - ); + passportData[`${fn}`].map((item) => { + if (!Array.isArray(item?.identicalUnits)) { + baseSums[`${item?.development}`] += parseInt(item?.identicalUnits); + } + }); // Format property names for passport, and filter out any entries with default sum = 0 const formattedSums: Record = {}; @@ -74,14 +102,19 @@ export function sumIdenticalUnitsByDevelopmentType( return formattedSums; } +interface FlattenOptions { + depth?: number; + path?: string | null; + separator?: string; +}; + /** * Flattens nested object so we can output passport variables like `{listFn}.{itemIndexAsText}.{fieldFn}` * Adapted from https://gist.github.com/penguinboy/762197 */ export function flatten>( - object: T, - path: string | null = null, - separator = ".", + object: T, + { depth = Infinity, path = null, separator = ".", }: FlattenOptions = {} ): T { return Object.keys(object).reduce((acc: T, key: string): T => { const value = object[key]; @@ -100,8 +133,8 @@ export function flatten>( !(Array.isArray(value) && value.length === 0), ].every(Boolean); - return isObject - ? { ...acc, ...flatten(value, newPath, separator) } + return (isObject && depth > 0) + ? { ...acc, ...flatten(value, { depth: depth - 1, path: newPath, separator }) } : { ...acc, [newPath]: value }; }, {} as T); } diff --git a/editor.planx.uk/src/ui/shared/Checkbox.tsx b/editor.planx.uk/src/ui/shared/Checkbox.tsx index 5637178092..37969b1e90 100644 --- a/editor.planx.uk/src/ui/shared/Checkbox.tsx +++ b/editor.planx.uk/src/ui/shared/Checkbox.tsx @@ -11,7 +11,7 @@ const Root = styled(Box)(({ theme }) => ({ height: 40, borderColor: theme.palette.text.primary, border: "2px solid", - background: "transparent", + backgroundColor: theme.palette.common.white, "&:focus-within": borderedFocusStyle, }));