diff --git a/editor.planx.uk/src/@planx/components/DateInput/Public.tsx b/editor.planx.uk/src/@planx/components/DateInput/Public.tsx index 90e80d35d5..f936c0f04c 100644 --- a/editor.planx.uk/src/@planx/components/DateInput/Public.tsx +++ b/editor.planx.uk/src/@planx/components/DateInput/Public.tsx @@ -27,7 +27,7 @@ const DateInputPublic: React.FC = (props) => { validateOnBlur: false, validateOnChange: false, validationSchema: object({ - date: dateRangeSchema({ min: props.min, max: props.max }), + date: dateRangeSchema(props), }), }); diff --git a/editor.planx.uk/src/@planx/components/DateInput/model.test.ts b/editor.planx.uk/src/@planx/components/DateInput/model.test.ts index 7d73193590..6415cb3bd8 100644 --- a/editor.planx.uk/src/@planx/components/DateInput/model.test.ts +++ b/editor.planx.uk/src/@planx/components/DateInput/model.test.ts @@ -1,4 +1,10 @@ -import { dateRangeSchema, dateSchema, paddedDate, parseDate } from "./model"; +import { + DateInput, + dateRangeSchema, + dateSchema, + paddedDate, + parseDate, +} from "./model"; describe("parseDate helper function", () => { it("returns a day value", () => { @@ -101,19 +107,22 @@ describe("dateSchema", () => { describe("dateRangeSchema", () => { test("basic validation", async () => { expect( - await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid( - "1995-06-15", - ), + await dateRangeSchema({ + min: "1990-01-01", + max: "1999-12-31", + } as DateInput).isValid("1995-06-15") ).toBe(true); expect( - await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid( - "2021-06-15", - ), + await dateRangeSchema({ + min: "1990-01-01", + max: "1999-12-31", + } as DateInput).isValid("2021-06-15") ).toBe(false); expect( - await dateRangeSchema({ min: "1990-01-01", max: "1999-12-31" }).isValid( - "1980-06-15", - ), + await dateRangeSchema({ + min: "1990-01-01", + max: "1999-12-31", + } as DateInput).isValid("1980-06-15") ).toBe(false); }); }); diff --git a/editor.planx.uk/src/@planx/components/DateInput/model.ts b/editor.planx.uk/src/@planx/components/DateInput/model.ts index f661f803e3..5309894533 100644 --- a/editor.planx.uk/src/@planx/components/DateInput/model.ts +++ b/editor.planx.uk/src/@planx/components/DateInput/model.ts @@ -97,10 +97,7 @@ export const dateSchema = () => { ); }; -export const dateRangeSchema: (params: { - min?: string; - max?: string; -}) => SchemaOf = (params) => +export const dateRangeSchema: (input: DateInput) => SchemaOf = (params) => dateSchema() .required("Enter a valid date in DD.MM.YYYY format") .test({ 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 961e219c8c..81a9865a7c 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx @@ -5,12 +5,14 @@ import Grid from "@mui/material/Grid"; 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 { getIn } from "formik"; import { get } from "lodash"; import React from "react"; import SelectInput from "ui/editor/SelectInput"; import InputLabel from "ui/public/InputLabel"; import ChecklistItem from "ui/shared/ChecklistItem"; +import DateInput from "ui/shared/DateInput"; import ErrorWrapper from "ui/shared/ErrorWrapper"; import Input from "ui/shared/Input"; import InputRowLabel from "ui/shared/InputRowLabel"; @@ -19,6 +21,7 @@ import { DESCRIPTION_TEXT, ERROR_MESSAGE } from "../../shared/constants"; import BasicRadio from "../../shared/Radio/BasicRadio"; import type { ChecklistField, + DateField, NumberField, QuestionField, TextField, @@ -225,3 +228,26 @@ export const ChecklistFieldInput: React.FC> = (props) => { ); }; + +export const DateFieldInput: React.FC> = ({ + id, + data, +}) => { + const { formik, activeIndex } = useListContext(); + + return ( + + + { + formik.setFieldValue(`userData[${activeIndex}]['${data.fn}']`, paddedDate(newDate, eventType)); + }} + error={get(formik.errors, ["userData", activeIndex, data.fn])} + id={id} + /> + + + ); +}; 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 444148bedc..911c28834a 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 @@ -385,9 +385,11 @@ describe("Form validation and error handling", () => { ); let errorMessages = getAllByTestId(/error-message-input/); + // One error per field, plus 3 for a date input (one per input) + const numberOfErrors = mockZooProps.schema.fields.length + 3; // Each field has an ErrorWrapper - expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length); + expect(errorMessages).toHaveLength(numberOfErrors); // All are empty initially errorMessages.forEach((message) => { @@ -398,10 +400,12 @@ describe("Form validation and error handling", () => { // Error wrappers persist errorMessages = getAllByTestId(/error-message-input/); - expect(errorMessages).toHaveLength(mockZooProps.schema.fields.length); + expect(errorMessages).toHaveLength(numberOfErrors); - // Each field is in an error state - errorMessages.forEach((message) => { + // Each field is in an error state, ignoring individual date input fields + const fieldErrors = errorMessages.slice(0, mockZooProps.schema.fields.length); + + fieldErrors.forEach((message) => { expect(message).not.toBeEmptyDOMElement(); }); }); @@ -495,6 +499,22 @@ describe("Form validation and error handling", () => { /Select at least one option/, ); }); + + test("date fields", async () => { + const { user, getByRole, getAllByTestId } = setup( + , + ); + + await user.click(getByRole("button", { name: /Save/ })); + + const dateInputErrorMessage = getAllByTestId( + /error-message-input-date-birthday/, + )[0]; + + expect(dateInputErrorMessage).toHaveTextContent( + /Date must include a day/, + ); + }); }); test("an error displays if the minimum number of items is not met", async () => { @@ -708,6 +728,13 @@ const fillInResponse = async (user: UserEvent) => { await user.click(eatCheckboxes[1]); await user.click(eatCheckboxes[2]); + const dayInput = screen.getByLabelText("Day"); + const monthInput = screen.getByLabelText("Month"); + const yearInput = screen.getByLabelText("Year"); + await user.type(dayInput, "14"); + await user.type(monthInput, "7"); + await user.type(yearInput, "1988"); + 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 52ae77b23e..0bd1c38ecc 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -21,6 +21,7 @@ import { formatSchemaDisplayValue } from "../utils"; import { ListProvider, useListContext } from "./Context"; import { ChecklistFieldInput, + DateFieldInput, NumberFieldInput, RadioFieldInput, SelectFieldInput, @@ -62,6 +63,8 @@ const InputField: React.FC = (props) => { return ; case "checklist": return ; + case "date": + return ; } }; diff --git a/editor.planx.uk/src/@planx/components/List/model.ts b/editor.planx.uk/src/@planx/components/List/model.ts index 759aae527b..f5278758e6 100644 --- a/editor.planx.uk/src/@planx/components/List/model.ts +++ b/editor.planx.uk/src/@planx/components/List/model.ts @@ -2,6 +2,10 @@ import { cloneDeep } from "lodash"; import { array, BaseSchema, object, ObjectSchema, string } from "yup"; import { checklistValidationSchema } from "../Checklist/model"; +import { + DateInput, + dateRangeSchema as dateValidationSchema, +} from "../DateInput/model"; import { NumberInput, numberInputValidationSchema } from "../NumberInput/model"; import { MoreInformation, Option, parseMoreInformation } from "../shared"; import { @@ -49,17 +53,23 @@ export type QuestionField = { type: "question"; data: QuestionInput & { fn: string }; }; + export type ChecklistField = { type: "checklist"; required?: true; data: ChecklistInput & { fn: string }; }; +export type DateField = { + type: "date"; + data: DateInput & { 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 | ChecklistField; +export type Field = TextField | NumberField | QuestionField | ChecklistField | DateField; /** * Models the form displayed to the user @@ -115,6 +125,9 @@ const generateValidationSchemaForFields = ( case "checklist": fieldSchemas[data.fn] = checklistValidationSchema(data); break; + case "date": + fieldSchemas[data.fn] = dateValidationSchema(data); + break; } }); 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 59394a6f31..eae39d39d0 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 @@ -70,10 +70,20 @@ export const Zoo: Schema = { { id: "meat", data: { text: "Meat" } }, { id: "leaves", data: { text: "Leaves" } }, { id: "bamboo", data: { text: "Bamboo" } }, - { id: "fruit", data: { text: "fruit" } }, + { id: "fruit", data: { text: "Fruit" } }, ], }, }, + // Date + { + type: "date", + data: { + title: "What's their birthday?", + fn: "birthday", + min: "1970-01-01", + max: "2999-12-31" + }, + }, ], min: 1, max: 3, @@ -97,6 +107,7 @@ export const mockZooPayload = { name: "Richard Parker", size: "Medium", food: ["meat", "leaves", "bamboo"], + birthday: "1988-07-14", }, { age: 10, @@ -105,6 +116,7 @@ export const mockZooPayload = { name: "Richard Parker", size: "Medium", food: ["meat", "leaves", "bamboo"], + birthday: "1988-07-14", }, ], "mockFn.one.age": 10, @@ -113,12 +125,14 @@ export const mockZooPayload = { "mockFn.one.name": "Richard Parker", "mockFn.one.size": "Medium", "mockFn.one.food": ["meat", "leaves", "bamboo"], + "mockFn.one.birthday": "1988-07-14", "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.two.birthday": "1988-07-14", "mockFn.total.listItems": 2, }, }; diff --git a/editor.planx.uk/src/@planx/components/List/utils.tsx b/editor.planx.uk/src/@planx/components/List/utils.tsx index c0cd78cb3d..ca3f48dd59 100644 --- a/editor.planx.uk/src/@planx/components/List/utils.tsx +++ b/editor.planx.uk/src/@planx/components/List/utils.tsx @@ -23,6 +23,7 @@ export function formatSchemaDisplayValue( case "number": return field.data.units ? `${value} ${field.data.units}` : value; case "text": + case "date": return value; case "checklist": { const matchingOptions = field.data.options.filter((option) =>