Skip to content

Commit

Permalink
chore: extend pay metadata (#4155)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamdelion authored Jan 15, 2025
1 parent 6aa1556 commit bd84ab9
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import DataObjectIcon from "@mui/icons-material/DataObject";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import Typography from "@mui/material/Typography";
import {
GovPayMetadata,
} from "@opensystemslab/planx-core/types";
import {
Pay,
REQUIRED_GOVPAY_METADATA,
} from "@planx/components/Pay/model";
import { GovPayMetadata } from "@opensystemslab/planx-core/types";
import { Pay } from "@planx/components/Pay/model";
import { useFormikContext } from "formik";
import React from "react";
import ListManager, {
Expand All @@ -20,65 +15,16 @@ import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";

import { isFieldDisabled, parseError, parseTouched } from "./helpers";

const GOVPAY_DOCS_URL =
"https://docs.payments.service.gov.uk/reporting/#add-more-information-to-a-payment-39-custom-metadata-39-or-39-reporting-columns-39";

type FormikGovPayMetadata =
export type FormikGovPayMetadata =
| Record<keyof GovPayMetadata, string>[]
| string
| undefined;

/**
* Helper method to handle Formik errors in arrays
* Required as errors can be at array-level or field-level and the useFormikContext hook cannot correctly type infer this from the validation schema
* Docs: https://formik.org/docs/api/fieldarray#fieldarray-validation-gotchas
*/
const parseError = (
errors: FormikGovPayMetadata,
index: number,
): string | undefined => {
// No errors
if (!errors) return;

// Array-level error - handled at a higher level
if (typeof errors === "string") return;

// No error for this field
if (!errors[index]) return;

// Specific field-level error
return errors[index].key || errors[index].value;
};

/**
* Helper method to handle Formik "touched" in arrays
* Please see parseError() for additional context
*/
const parseTouched = (
touched: string | undefined | FormikGovPayMetadata,
index: number,
): string | undefined => {
// No errors
if (!touched) return;

// Array-level error - handled at a higher level
if (typeof touched === "string") return;

// No error for this field
if (!touched[index]) return;

// Specific field-level error
return touched[index].key && touched[index].value;
};

/**
* Disable required fields so they cannot be edited
* Only disable first instance, otherwise any field beginning with a required field will be disabled, and user will not be able to fix their mistake as the delete icon is also disabled
*/
const isFieldDisabled = (key: string, index: number) =>
REQUIRED_GOVPAY_METADATA.includes(key) &&
index === REQUIRED_GOVPAY_METADATA.indexOf(key);

function GovPayMetadataEditor(props: ListManagerEditorProps<GovPayMetadata>) {
const { key: currKey, value: currVal } = props.value;
const isDisabled = isFieldDisabled(currKey, props.index);
Expand Down Expand Up @@ -122,17 +68,15 @@ function GovPayMetadataEditor(props: ListManagerEditorProps<GovPayMetadata>) {
}

export const GovPayMetadataSection: React.FC = () => {
const { errors, setFieldValue, setTouched, touched, values } = useFormikContext<Pay>();
const { errors, setFieldValue, setTouched, touched, values } =
useFormikContext<Pay>();

return (
<ModalSection>
<ModalSectionContent
title="GOV.UK Pay Metadata"
Icon={DataObjectIcon}
>
<ModalSectionContent title="GOV.UK Pay Metadata" Icon={DataObjectIcon}>
<Typography variant="subtitle2" sx={{ mb: 2 }}>
Include metadata alongside payments, such as VAT codes, cost
centers, or ledger codes. See{" "}
Include metadata alongside payments, such as VAT codes, cost centers,
or ledger codes. See{" "}
<Link
href={GOVPAY_DOCS_URL}
target="_blank"
Expand All @@ -143,13 +87,12 @@ export const GovPayMetadataSection: React.FC = () => {
for more details.
</Typography>
<Typography variant="subtitle2" sx={{ mb: 2 }}>
Any values beginning with @ will be dynamically read from data
values set throughout the flow.
Any values beginning with @ will be dynamically read from data values
set throughout the flow.
</Typography>
<ErrorWrapper
error={
typeof errors.govPayMetadata === "string" &&
touched.govPayMetadata
typeof errors.govPayMetadata === "string" && touched.govPayMetadata
? errors.govPayMetadata
: undefined
}
Expand Down Expand Up @@ -181,7 +124,7 @@ export const GovPayMetadataSection: React.FC = () => {
</Typography>
</Box>
<ListManager
maxItems={10}
maxItems={15}
disableDragAndDrop
values={values.govPayMetadata || []}
onChange={(metadata) => {
Expand All @@ -192,13 +135,11 @@ export const GovPayMetadataSection: React.FC = () => {
setTouched({});
return { key: "", value: "" };
}}
isFieldDisabled={({ key }, index) =>
isFieldDisabled(key, index)
}
isFieldDisabled={({ key }, index) => isFieldDisabled(key, index)}
/>
</>
</ErrorWrapper>
</ModalSectionContent>
</ModalSection>
)
}
);
};
51 changes: 51 additions & 0 deletions editor.planx.uk/src/@planx/components/Pay/Editor/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { REQUIRED_GOVPAY_METADATA } from "../model";
import { FormikGovPayMetadata } from "./GovPayMetadataSection";

/**
* Helper method to handle Formik errors in arrays
* Required as errors can be at array-level or field-level and the useFormikContext hook cannot correctly type infer this from the validation schema
* Docs: https://formik.org/docs/api/fieldarray#fieldarray-validation-gotchas
*/
export const parseError = (
errors: FormikGovPayMetadata,
index: number,
): string | undefined => {
// No errors
if (!errors) return;

// Array-level error - handled at a higher level
if (typeof errors === "string") return;

// No error for this field
if (!errors[index]) return;

// Specific field-level error
return errors[index].key || errors[index].value;
};
/**
* Helper method to handle Formik "touched" in arrays
* Please see parseError() for additional context
*/
export const parseTouched = (
touched: string | undefined | FormikGovPayMetadata,
index: number,
): string | undefined => {
// No errors
if (!touched) return;

// Array-level error - handled at a higher level
if (typeof touched === "string") return;

// No error for this field
if (!touched[index]) return;

// Specific field-level error
return touched[index].key && touched[index].value;
};
/**
* Disable required fields so they cannot be edited
* Only disable first instance, otherwise any field beginning with a required field will be disabled, and user will not be able to fix their mistake as the delete icon is also disabled
*/
export const isFieldDisabled = (key: string, index: number) =>
REQUIRED_GOVPAY_METADATA.includes(key) &&
index === REQUIRED_GOVPAY_METADATA.indexOf(key);
14 changes: 10 additions & 4 deletions editor.planx.uk/src/@planx/components/Pay/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe("GovPayMetadata Schema", () => {
expect(errors[0]).toMatch(/Keys must be unique/);
});

test("max 10 entries can be added", async () => {
test("max 15 entries can be added", async () => {
const input = [
...defaults,
{ key: "four", value: "someValue" },
Expand All @@ -107,19 +107,25 @@ describe("GovPayMetadata Schema", () => {
{ key: "eight", value: "someValue" },
{ key: "nine", value: "someValue" },
{ key: "ten", value: "someValue" },
{ key: "eleven", value: "someValue" },
{ key: "twelve", value: "someValue" },
{ key: "thirteen", value: "someValue" },
{ key: "fourteen", value: "someValue" },
{ key: "fifteen", value: "someValue" },
];

const result = await validate(input);

// No errors, input returned from validation
expect(result).toEqual(input);

// Try 11 total values
// Try 16 total values - i.e. one more than permitted
const errors = await validate([
...input,
{ key: "eleven", value: "someValue" },
{ key: "sixteen", value: "someValue" },
]);

expect(errors).toHaveLength(1);
expect(errors[0]).toMatch(/A maximum of 10 fields can be set as metadata/);
expect(errors[0]).toMatch(/A maximum of 15 fields can be set as metadata/);
});
});
2 changes: 1 addition & 1 deletion editor.planx.uk/src/@planx/components/Pay/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const govPayMetadataSchema = array(
}),
}),
)
.max(10, "A maximum of 10 fields can be set as metadata")
.max(15, "A maximum of 15 fields can be set as metadata")
.test({
name: "unique-keys",
message: "Keys must be unique",
Expand Down

0 comments on commit bd84ab9

Please sign in to comment.