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(works-to-trees): Add DateInputField to List component #3519

Merged
merged 4 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion editor.planx.uk/src/@planx/components/DateInput/Public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DateInputPublic: React.FC<Props> = (props) => {
validateOnBlur: false,
validateOnChange: false,
validationSchema: object({
date: dateRangeSchema({ min: props.min, max: props.max }),
date: dateRangeSchema(props),
}),
});

Expand Down
29 changes: 19 additions & 10 deletions editor.planx.uk/src/@planx/components/DateInput/model.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
Expand Down
5 changes: 1 addition & 4 deletions editor.planx.uk/src/@planx/components/DateInput/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@ export const dateSchema = () => {
);
};

export const dateRangeSchema: (params: {
min?: string;
max?: string;
}) => SchemaOf<string> = (params) =>
export const dateRangeSchema: (input: DateInput) => SchemaOf<string> = (params) =>
dateSchema()
.required("Enter a valid date in DD.MM.YYYY format")
.test({
Expand Down
26 changes: 26 additions & 0 deletions editor.planx.uk/src/@planx/components/List/Public/Fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -225,3 +228,26 @@ export const ChecklistFieldInput: React.FC<Props<ChecklistField>> = (props) => {
</InputLabel>
);
};

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we think next about how to better share "schemas" between components that may use them (List here, MapAndLabel next) - I'm very interested if this useListContext() could be extracted out into a useSchemaContext or similar ?? Then the "Schema" becomes a Context & formik aware component that can be referenced in many components?

Was anticipating "fields" would be something I could externally reference/"just pull into" initial MapAndLabel setup - but this bit of context very much collided / lead me to initially just duplicating field definitions with a minor useMapAndLabelContext() difference.

Definitely not a thing to address in this PR, just starting to think bigger direction!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally - this is the exact issue I hit when I was trying to rush out Page before show and tell 😅

There's really two layers of responsibility here - something which handles fields, and something which handles lists (or the wider component). So errors relating to min, max, etc would be in the lists "layer" and the field validation logic etc can we handled elsewhere.

Planning on taking a look at this next pretty much as I know it's a bit of a blocker atm 👍


return (
<InputLabel label={data.title} htmlFor={id}>
<Box sx={{ display: "flex", alignItems: "baseline" }}>
<DateInput
value={formik.values.userData[activeIndex][data.fn] as string}
bordered
onChange={(newDate: string, eventType: string) => {
formik.setFieldValue(`userData[${activeIndex}]['${data.fn}']`, paddedDate(newDate, eventType));
}}
error={get(formik.errors, ["userData", activeIndex, data.fn])}
id={id}
/>
</Box>
</InputLabel>
);
};
35 changes: 31 additions & 4 deletions editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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();
});
});
Expand Down Expand Up @@ -495,6 +499,22 @@ describe("Form validation and error handling", () => {
/Select at least one option/,
);
});

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

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 () => {
Expand Down Expand Up @@ -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/,
});
Expand Down
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,
DateFieldInput,
NumberFieldInput,
RadioFieldInput,
SelectFieldInput,
Expand Down Expand Up @@ -62,6 +63,8 @@ const InputField: React.FC<Field> = (props) => {
return <SelectFieldInput id={inputFieldId} {...props} />;
case "checklist":
return <ChecklistFieldInput id={inputFieldId} {...props} />;
case "date":
return <DateFieldInput id={inputFieldId} {...props} />;
}
};

Expand Down
15 changes: 14 additions & 1 deletion editor.planx.uk/src/@planx/components/List/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -115,6 +125,9 @@ const generateValidationSchemaForFields = (
case "checklist":
fieldSchemas[data.fn] = checklistValidationSchema(data);
break;
case "date":
fieldSchemas[data.fn] = dateValidationSchema(data);
break;
}
});

Expand Down
16 changes: 15 additions & 1 deletion editor.planx.uk/src/@planx/components/List/schemas/mocks/Zoo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -97,6 +107,7 @@ export const mockZooPayload = {
name: "Richard Parker",
size: "Medium",
food: ["meat", "leaves", "bamboo"],
birthday: "1988-07-14",
},
{
age: 10,
Expand All @@ -105,6 +116,7 @@ export const mockZooPayload = {
name: "Richard Parker",
size: "Medium",
food: ["meat", "leaves", "bamboo"],
birthday: "1988-07-14",
},
],
"mockFn.one.age": 10,
Expand All @@ -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": "[email protected]",
"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,
},
};
1 change: 1 addition & 0 deletions editor.planx.uk/src/@planx/components/List/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Loading