From 5422d6c1800a5ff961270e1effc224718558926a Mon Sep 17 00:00:00 2001 From: Pete <107579368+thiessenp-cds@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:23:20 -0400 Subject: [PATCH] feat: Updates Repeating Sets, Pages and Rules to work together on the Review page and form output (#4186) * Initial commit * Start on Review page formatting * Updates build form to handle pages and repeating sets * Updates build form types and simplifies logic a bit --------- Co-authored-by: Vivian Nobrega <43480681+ShadeWyrm@users.noreply.github.com> --- .../id/[...props]/lib/buildFormDataObject.ts | 13 +- .../clientComponents/forms/Review/Review.tsx | 132 ++++++++++++++---- 2 files changed, 111 insertions(+), 34 deletions(-) diff --git a/app/(gcforms)/[locale]/(form filler)/id/[...props]/lib/buildFormDataObject.ts b/app/(gcforms)/[locale]/(form filler)/id/[...props]/lib/buildFormDataObject.ts index 932c548ffb..ba30d2a990 100644 --- a/app/(gcforms)/[locale]/(form filler)/id/[...props]/lib/buildFormDataObject.ts +++ b/app/(gcforms)/[locale]/(form filler)/id/[...props]/lib/buildFormDataObject.ts @@ -33,11 +33,10 @@ export function buildFormDataObject(formRecord: PublicFormRecord, values: Respon function _handleDynamicRowTypeIfNeeded( element: FormElement, - value: Response + value: Response | Responses[] ): [string, string | FileInputResponse][] { - if (element.type === FormElementTypes.dynamicRow) { + if (element.type === FormElementTypes.dynamicRow && Array.isArray(value)) { if (element.properties.subElements === undefined) return []; - const responses = value as Responses[]; const subElements = element.properties.subElements; @@ -66,15 +65,15 @@ function _handleDynamicRowTypeIfNeeded( // `flat` function is needed because we use a `map` in a `map`. .flat() ); - } else { - const result = _handleFormDataType(element, value); - return result ? [result] : []; } + + const result = _handleFormDataType(element, value); + return result ? [result] : []; } function _handleFormDataType( element: FormElement, - value: Response + value: Response | Responses[] ): [string, string | FileInputResponse] | undefined { switch (element.type) { case FormElementTypes.textField: diff --git a/components/clientComponents/forms/Review/Review.tsx b/components/clientComponents/forms/Review/Review.tsx index fda92b65dd..24cf6baede 100644 --- a/components/clientComponents/forms/Review/Review.tsx +++ b/components/clientComponents/forms/Review/Review.tsx @@ -4,7 +4,7 @@ import { Button } from "@clientComponents/globals"; import { Theme } from "@clientComponents/globals/Buttons/themes"; import { useFocusIt } from "@lib/hooks/useFocusIt"; import { useGCFormsContext } from "@lib/hooks/useGCFormContext"; -import { FormElement } from "@lib/types"; +import { FileInputResponse, FormElement, FormElementTypes } from "@lib/types"; import { Language } from "@lib/types/form-builder-types"; import { getLocalizedProperty } from "@lib/utils"; import { @@ -14,31 +14,39 @@ import { getElementIdsAsNumber, Group, } from "@lib/formContext"; +import { randomId } from "@lib/client/clientHelpers"; type ReviewItem = { id: string; name: string; title: string; - elements: { - elementId: number; - title: string; - values: string; - }[]; + elements: Element[]; }; -function getFormElementValues(elementName: number | null, formValues: void | FormValues) { - const value = formValues[elementName as keyof typeof formValues]; - if (Array.isArray(value)) { - return (value as Array).join(", ") || "-"; - } - return value || "-"; -} +type Element = { + title: string; + values: string | FileInputResponse | Element[]; +}; -function getFormElementTitle(formElementId: number, formElements: FormElement[], lang: string) { - const formElement = formElements.find((item) => item.id === formElementId); - return formElement - ? (formElement.properties?.[getLocalizedProperty("title", lang)] as string) - : "-"; +function formatElementValues(values: Element["values"]) { + if (!values) { + return "-"; + } + // Case of a File upload + if ((values as FileInputResponse).based64EncodedFile !== undefined) { + const file = values as FileInputResponse; + if (!file.name || !file.size || file.size < 0) { + return "-"; + } + const fileSizeInMB = (file.size / 1024 / 1024).toFixed(2); + return `${file.name} (${fileSizeInMB} MB)`; + } + // Case of an array like element e.g. checkbox + if (Array.isArray(values)) { + return values.join(", ") || "-"; + } + // Case of a single value element e.g. input + return String(values); } function getReviewItemElements( @@ -52,14 +60,48 @@ function getReviewItemElements( const shownElementIds = getElementIdsAsNumber( filterValuesForShownElements(groupElements, shownFormElements) ); - const result = shownElementIds.map((elementId) => { + return shownElementIds.map((elementId) => { + const element = formElements.find((item) => item.id === elementId); + let resultValues: string | Element[] = formatElementValues( + formValues[elementId as unknown as keyof typeof formValues] as Element["values"] + ); + // Handle any Sub Elements. Note Sub Elements = Dynamic Rows = Repeating Sets + if (element?.type === FormElementTypes.dynamicRow) { + resultValues = []; + const parentId = element.id; + const parentTitle = element.properties?.[getLocalizedProperty("title", lang)]; + const subElements = element.properties?.subElements; + // Use FormValues as the source of truth and for each FormValue value, map the related + // subElement title to the FormValue value + const subElementValues = (formValues[parentId] as string[]).map( + (valueRows, valueRowsIndex) => { + const subElementsTitle = `${parentTitle} - ${valueRowsIndex + 1}`; + const valueRowsAsArray = Object.keys(valueRows).map( + (key) => valueRows[key as keyof typeof valueRows] + ); + // Match the FormValue index to the subElement index to assign the Element title + const titlesMappedToValues = valueRowsAsArray.map((formValue, valueRowIndex) => { + return { + title: subElements?.[valueRowIndex].properties?.[getLocalizedProperty("title", lang)], + values: formValue, + }; + }); + return { + title: subElementsTitle, + values: titlesMappedToValues, + } as Element; + } + ); + resultValues.push({ + title: parentTitle as string, + values: subElementValues, + }); + } return { - elementId, - title: getFormElementTitle(elementId, formElements, lang), - values: getFormElementValues(elementId, formValues), + title: (element?.properties?.[getLocalizedProperty("title", lang)] as string) || "-", + values: resultValues, }; }); - return result; } export const Review = ({ language }: { language: Language }): React.ReactElement => { @@ -78,7 +120,7 @@ export const Review = ({ language }: { language: Language }): React.ReactElement return groupHistory .filter((key) => key !== "review") // Removed to avoid showing as a group .map((groupId) => { - const group: Group = groups[groupId as keyof typeof groups]; + const group: Group = groups[groupId as keyof typeof groups] || {}; return { id: groupId, name: group.name, @@ -140,10 +182,13 @@ const QuestionsAnswersList = ({ reviewItem }: { reviewItem: ReviewItem }): React
{Array.isArray(reviewItem.elements) && reviewItem.elements.map((reviewElement) => { + if (Array.isArray(reviewElement.values)) { + return ; + } return ( -
-
{reviewElement.title}
-
{reviewElement.values}
+
+
{reviewElement.title}
+
{formatElementValues(reviewElement.values)}
); })} @@ -151,6 +196,39 @@ const QuestionsAnswersList = ({ reviewItem }: { reviewItem: ReviewItem }): React ); }; +// Handle formatting Sub Elements. Note Sub Elements = Dynamic Rows = Repeating Sets. +const SubElements = ({ elements }: { elements: Element[] }) => { + return elements?.map((subElementItem) => { + return (subElementItem.values as Element[])?.map((element) => { + if (Array.isArray(element.values)) { + const dlId = randomId(); + // Create a nested DL for each Sub Element list + return ( +
+

+ {element.title} +

+ {(element.values as Element[]).map((elementValues) => { + return ( +
+
{elementValues.title}
+
{formatElementValues(elementValues.values)}
+
+ ); + })} +
+ ); + } + return ( +
+
{element.title}
+
{formatElementValues(element.values)}
+
+ ); + }); + }); +}; + const EditButton = ({ reviewItem, theme,