From 300de777580b867791fa308aba52172e86f96add Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Thu, 6 Jun 2024 10:00:13 +0200 Subject: [PATCH] feat: add flattened passport structure for List items that is compatible with Calculate (#3249) --- .../@planx/components/List/Public/Context.tsx | 20 ++++- .../components/List/Public/index.test.tsx | 12 +++ .../src/@planx/components/List/utils.ts | 87 +++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 editor.planx.uk/src/@planx/components/List/utils.ts 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 e71a7b1363..6bdb461dd8 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -18,6 +18,7 @@ import { Schema, UserData, } from "../model"; +import { flatten } from "../utils"; interface ListContextValue { schema: Schema; @@ -131,7 +132,24 @@ export const ListProvider: React.FC = (props) => { userData: getInitialValues(), }, onSubmit: (values) => { - handleSubmit?.(makeData(props, values.userData)); + // defaultPassportData is used when coming "back" + const defaultPassportData = makeData(props, values.userData)?.["data"]; + + // flattenedPassportData makes individual list items compatible with Calculate components + const flattenedPassportData = flatten(defaultPassportData); + + // basic example of general summary stats we can add onSubmit + const summaries = { + [`${props.fn}.count`]: defaultPassportData[`${props.fn}`].length, + }; + + handleSubmit?.({ + data: { + ...defaultPassportData, + ...flattenedPassportData, + ...summaries, + }, + }); }, validateOnBlur: false, validateOnChange: false, 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 ee9178be5c..83073e6250 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 @@ -33,6 +33,17 @@ const mockPayload = { size: "Medium", }, ], + "mockFn.one.age": 10, + "mockFn.one.cuteness": "Very", + "mockFn.one.email": "richard.parker@pi.com", + "mockFn.one.name": "Richard Parker", + "mockFn.one.size": "Medium", + "mockFn.two.age": 10, + "mockFn.two.cuteness": "Very", + "mockFn.two.email": "richard.parker@pi.com", + "mockFn.two.name": "Richard Parker", + "mockFn.two.size": "Medium", + "mockFn.count": 2, }, }; @@ -355,6 +366,7 @@ describe("Form validation and error handling", () => { test.todo("number fields use existing validation schemas"); test.todo("question fields use validation schema"); test.todo("unique constraints are enforced on question where this is set"); + test.todo("optional fields can be empty when saving an item"); test.todo("an error displays if the minimum number of items is not met"); test.todo("an error displays if the maximum number of items is exceeded"); test.todo( diff --git a/editor.planx.uk/src/@planx/components/List/utils.ts b/editor.planx.uk/src/@planx/components/List/utils.ts new file mode 100644 index 0000000000..231f676f76 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/List/utils.ts @@ -0,0 +1,87 @@ +// 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 = ".", +): T { + return Object.keys(object).reduce((acc: T, key: string): T => { + const value = object[key]; + + // If the key is a whole number, convert to text before setting newPath + // eg because Calculate/MathJS cannot automate passport variables with number segments + if (/^-?\d+$/.test(key)) { + key = convertNumberToText(parseInt(key) + 1); + } + + const newPath = [path, key].filter(Boolean).join(separator); + + const isObject = [ + typeof value === "object", + value !== null, + !(Array.isArray(value) && value.length === 0), + ].every(Boolean); + + return isObject + ? { ...acc, ...flatten(value, newPath, separator) } + : { ...acc, [newPath]: value }; + }, {} as T); +} + +// Convert a whole number up to 99 to a spelled-out word (eg 34 => 'thirtyfour') +// Adapted from https://stackoverflow.com/questions/5529934/javascript-numbers-to-words +const ones = [ + "", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", +]; +const tens = [ + "", + "", + "twenty", + "thirty", + "forty", + "fifty", + "sixty", + "seventy", + "eighty", + "ninety", +]; +const teens = [ + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen", +]; + +function convertTens(num: number): string { + if (num < 10) { + return ones[num]; + } else if (num >= 10 && num < 20) { + return teens[num - 10]; + } else { + // format as compound string - eg "thirtyfour" instead of "thirty four" + return tens[Math.floor(num / 10)] + ones[num % 10]; + } +} + +function convertNumberToText(num: number): string { + if (num == 0) { + return "zero"; + } else { + return convertTens(num); + } +}