From 78a58716489bd4a9ca80157a802a03fe52b7b51a Mon Sep 17 00:00:00 2001 From: scottheng96 Date: Tue, 17 Dec 2024 09:08:36 +0800 Subject: [PATCH] consolidated fe and aligned be - failing on submission still --- .../backend/helpers/generate-form-data.ts | 9 +- .../FieldRow/FieldRowContainer.tsx | 4 +- .../EditAddress/EditAddress.stories.tsx | 6 +- .../EditAddress/EditAddress.tsx | 8 +- .../components/FormFields/FieldFactory.tsx | 4 +- .../public-form/utils/createSubmission.ts | 13 +- .../public-form/utils/inputTransformation.ts | 41 +++- frontend/src/services/ApiService.ts | 1 + .../Field/Address/AddressField.stories.tsx | 142 ++++++++------ .../templates/Field/Address/AddressField.tsx | 175 +++++++++++------- frontend/src/templates/Field/Address/index.ts | 2 +- .../ChildrenCompoundField.tsx | 2 +- frontend/src/templates/Field/index.ts | 4 +- frontend/src/templates/Field/types.ts | 19 +- frontend/src/utils/fieldValidation.ts | 10 +- shared/constants/field/basic.ts | 2 +- shared/types/field/addressField.ts | 12 +- shared/types/field/index.ts | 4 +- shared/types/response-v3.ts | 18 +- shared/types/response.ts | 7 +- src/app/models/field/addressField.ts | 8 +- src/app/modules/form/form.service.ts | 4 +- .../email-submission/email-submission.util.ts | 26 +++ .../multirespondent-submission.utils.ts | 4 +- .../modules/submission/submission.types.ts | 4 + .../answerValidator.factory.ts | 12 +- .../field-validation.guards.ts | 14 ++ .../validators/addressValidator.ts | 54 ++++-- src/types/field/addressField.ts | 6 +- src/types/field/index.ts | 6 +- src/types/response/index.ts | 3 + 31 files changed, 396 insertions(+), 228 deletions(-) diff --git a/__tests__/unit/backend/helpers/generate-form-data.ts b/__tests__/unit/backend/helpers/generate-form-data.ts index ef0b5793f1..bde03c077f 100644 --- a/__tests__/unit/backend/helpers/generate-form-data.ts +++ b/__tests__/unit/backend/helpers/generate-form-data.ts @@ -275,9 +275,12 @@ export const generateNewSingleAnswerResponse = ( customParams?: Partial, ): ProcessedSingleAnswerResponse => { if ( - [BasicField.Attachment, BasicField.Table, BasicField.Checkbox].includes( - fieldType, - ) + [ + BasicField.Attachment, + BasicField.Table, + BasicField.Checkbox, + BasicField.Address, + ].includes(fieldType) ) { throw new Error( 'Call the custom response generator functions for attachment, table and checkbox.', diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx index 1b59c5556f..67a1b7f96e 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx @@ -23,7 +23,7 @@ import { useToast } from '~hooks/useToast' import IconButton from '~components/IconButton' import Tooltip from '~components/Tooltip' import { - AddressField, + AddressCompoundField, AttachmentField, CheckboxField, ChildrenCompoundField, @@ -514,6 +514,6 @@ const FieldRow = ({ field, ...rest }: FieldRowProps) => { case BasicField.Children: return case BasicField.Address: - return + return } } diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.stories.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.stories.tsx index 76a723ba31..ee8815ec5a 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.stories.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryFn } from '@storybook/react' -import { AddressFieldBase, BasicField } from '~shared/types' +import { AddressCompoundFieldBase, BasicField } from '~shared/types' import { createFormBuilderMocks } from '~/mocks/msw/handlers/admin-form' @@ -8,7 +8,7 @@ import { EditFieldDrawerDecorator, StoryRouter } from '~utils/storybook' import { EditAddress, EditAddressProps } from './EditAddress' -const DEFAULT_ADDRESS_FIELD: AddressFieldBase = { +const DEFAULT_ADDRESS_FIELD: AddressCompoundFieldBase = { title: 'Local address', description: '', required: true, @@ -38,7 +38,7 @@ export default { } as Meta interface StoryArgs { - field: AddressFieldBase + field: AddressCompoundFieldBase } const Template: StoryFn = ({ field }) => { diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.tsx index 98d7395449..69d1f2949e 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditAddress/EditAddress.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { FormControl } from '@chakra-ui/react' import { extend, pick } from 'lodash' -import { AddressFieldBase } from '~shared/types/field' +import { AddressCompoundFieldBase } from '~shared/types/field' import { createBaseValidationRules } from '~utils/fieldValidation' import FormErrorMessage from '~components/FormControl/FormErrorMessage' @@ -17,10 +17,10 @@ import { FormFieldDrawerActions } from '../common/FormFieldDrawerActions' import { EditFieldProps } from '../common/types' import { useEditFieldForm } from '../common/useEditFieldForm' -export type EditAddressProps = EditFieldProps +export type EditAddressProps = EditFieldProps type EditAddressInputs = Pick< - AddressFieldBase, + AddressCompoundFieldBase, 'title' | 'description' | 'required' > @@ -37,7 +37,7 @@ export const EditAddress = ({ field }: EditAddressProps): JSX.Element => { isLoading, handleCancel, setValue, - } = useEditFieldForm({ + } = useEditFieldForm({ field, transform: { input: (inputField) => diff --git a/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx b/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx index 4b9cfc7c8c..aa58e1b426 100644 --- a/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx +++ b/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx @@ -4,7 +4,7 @@ import { BasicField } from '~shared/types/field' import { FormColorTheme, FormResponseMode } from '~shared/types/form' import { - AddressField, + AddressCompoundField, AttachmentField, CheckboxField, ChildrenCompoundField, @@ -121,7 +121,7 @@ export const FieldFactory = memo( case BasicField.Table: return case BasicField.Address: - return + return case BasicField.Children: return ( transformInputsToOutputs(ff, formInputs[ff._id])) .filter((output): output is FieldResponse => output !== null) + const v = validateResponses(transformedResponses) + console.log('testing') + console.log(v) return validateResponses(transformedResponses) } @@ -288,14 +292,7 @@ const createResponsesV3 = ( if (!input) break returnedInputs[ff._id] = { fieldType: ff.fieldType, - answer: { - postalCode: input.postalCode, - blockNumber: input.blockNumber, - streetName: input.streetName, - buildingName: input.buildingName, - levelNumber: input.levelNumber, - unitNumber: input.unitNumber, - }, + answer: input, } as FieldResponseV3 break } diff --git a/frontend/src/features/public-form/utils/inputTransformation.ts b/frontend/src/features/public-form/utils/inputTransformation.ts index 83ea4315cb..df6f0b3181 100644 --- a/frontend/src/features/public-form/utils/inputTransformation.ts +++ b/frontend/src/features/public-form/utils/inputTransformation.ts @@ -1,9 +1,10 @@ +import { Address } from 'cluster' import { format, parse } from 'date-fns' import { times } from 'lodash' import { DATE_PARSE_FORMAT } from '~shared/constants/dates' import { - AddressFieldResponseV3, + AddressCompoundFieldResponseV3, AttachmentFieldResponseV3, CheckboxFieldResponsesV3, ChildrenCompoundFieldResponsesV3, @@ -13,7 +14,11 @@ import { VerifiableFieldResponseV3, YesNoFieldResponseV3, } from '~shared/types' -import { BasicField, FormFieldDto } from '~shared/types/field' +import { + AddressAttributes, + BasicField, + FormFieldDto, +} from '~shared/types/field' import { AddressResponse, AttachmentResponse, @@ -31,8 +36,8 @@ import { CHECKBOX_OTHERS_INPUT_VALUE } from '~templates/Field/Checkbox/constants import { RADIO_OTHERS_INPUT_VALUE } from '~templates/Field/Radio/constants' import { createTableRow } from '~templates/Field/Table/utils/createRow' import { - AddressFieldSchema, - AddressFieldValues, + AddressCompoundFieldSchema, + AddressCompoundFieldValues, AttachmentFieldSchema, BaseFieldOutput, CheckboxFieldSchema, @@ -222,14 +227,32 @@ const transformToChildOutput = ( } const transformToAddressOutput = ( - schema: AddressFieldSchema, - input?: AddressFieldValues | AddressFieldResponseV3, + schema: AddressCompoundFieldSchema, + input?: AddressCompoundFieldValues | AddressCompoundFieldResponseV3, ): AddressResponse => { - let answer = '' - if (input?.postalCode !== undefined) answer = input.postalCode // TODO: do for all input fields, just using postalCode to test now + // let answerArray: AddressAttributes + const answerArray: string[][] = [] + // if (input !== undefined) { + // answerArray = input.addressSubFields + // } else { + // answerArray = { + // postalCode: '', + // blockNumber: '', + // streetName: '', + // buildingName: '', + // levelNumber: '', + // unitNumber: '', + // } + // } + if (input !== undefined) { + Object.entries(input.addressSubFields).map(([key, value]) => + answerArray.push([`${key}: ${value}`]), + ) + } + return { ...pickBaseOutputFromSchema(schema), - answer, + answerArray, } } diff --git a/frontend/src/services/ApiService.ts b/frontend/src/services/ApiService.ts index e4c50470a2..c80dc9e541 100644 --- a/frontend/src/services/ApiService.ts +++ b/frontend/src/services/ApiService.ts @@ -109,6 +109,7 @@ export const processFetchResponse = async (response: Response) => { throw new Error(`Non-2XX response: ${response.status}`) } else { const data = await response.json() + // console.log(data) return data } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/frontend/src/templates/Field/Address/AddressField.stories.tsx b/frontend/src/templates/Field/Address/AddressField.stories.tsx index cbf0323fcc..79ba98d0c0 100644 --- a/frontend/src/templates/Field/Address/AddressField.stories.tsx +++ b/frontend/src/templates/Field/Address/AddressField.stories.tsx @@ -8,14 +8,14 @@ import { BasicField } from '~shared/types/field' import Button from '~components/Button' -import { AddressFieldInput, AddressFieldSchema } from '../types' +import { AddressCompoundFieldInput, AddressCompoundFieldSchema } from '../types' import { - AddressField as AddressFieldComponent, - AddressFieldProps, + AddressCompoundField as AddressFieldComponent, + AddressCompoundFieldProps, } from './AddressField' -const baseSchema: AddressFieldSchema = { +const baseSchema: AddressCompoundFieldSchema = { title: 'Local address', description: '', required: true, @@ -43,8 +43,8 @@ export default { }, } as Meta -interface StoryAddressFieldProps extends AddressFieldProps { - defaultValue?: AddressFieldInput +interface StoryAddressFieldProps extends AddressCompoundFieldProps { + defaultValue?: AddressCompoundFieldInput apiError?: string | null } @@ -52,26 +52,20 @@ const Template: StoryFn = ({ defaultValue, ...args }) => { - const formMethods = useForm({ + const formMethods = useForm({ defaultValues: defaultValue, }) const [submitValues, setSubmitValues] = useState() - const onSubmit = (values: Record) => { - setSubmitValues(values[args.schema._id] || 'Nothing was selected') + // const onSubmit = (data: unknown) => alert(JSON.stringify(data)) + const onSubmit = (values: AddressCompoundFieldInput) => { + const fieldValue = values[args.schema._id]?.addressSubFields || {} + const hasValues = Object.values(fieldValue).some((val) => val?.trim()) + setSubmitValues( + hasValues ? JSON.stringify(fieldValue) : 'Nothing was selected', + ) } - - useEffect(() => { - if (defaultValue) { - Object.entries(defaultValue).forEach(([key, value]) => { - // Type assertion to ensure `key` is one of the valid keys of `AddressFieldValues` - formMethods.setValue(key as keyof AddressFieldInput, value) - }) - formMethods.trigger() - } - }, [defaultValue, formMethods]) - return (
@@ -94,12 +88,16 @@ export const WithValues = Template.bind({}) WithValues.args = { schema: baseSchema, defaultValue: { - postalCode: '123456', - blockNumber: '1', - streetName: 'Bukit Batok Street', - buildingName: '50', - levelNumber: '04', - unitNumber: '5A', + [baseSchema._id]: { + addressSubFields: { + postalCode: '123456', + blockNumber: '1', + streetName: 'Bukit Batok Street', + buildingName: '50', + levelNumber: '04', + unitNumber: '5A', + }, + }, }, } @@ -107,30 +105,50 @@ export const ValidationRequired = Template.bind({}) ValidationRequired.args = { schema: baseSchema, defaultValue: { - postalCode: '', - blockNumber: '', - streetName: '', - buildingName: '', - levelNumber: '', - unitNumber: '', + [baseSchema._id]: { + addressSubFields: { + postalCode: '', + blockNumber: '', + streetName: '', + buildingName: '', + levelNumber: '', + unitNumber: '', + }, + }, }, } export const ValidationNotRequired = Template.bind({}) ValidationNotRequired.args = { schema: { ...baseSchema, required: false }, + // defaultValue: { + // [baseSchema._id]: { + // addressSubFields: { + // postalCode: '', + // blockNumber: '', + // streetName: '', + // buildingName: '', + // levelNumber: '', + // unitNumber: '', + // }, + // }, + // }, } export const InvalidPostalCode = Template.bind({}) InvalidPostalCode.args = { schema: baseSchema, defaultValue: { - postalCode: '123az#$%', - blockNumber: '', - streetName: '', - buildingName: '', - levelNumber: '', - unitNumber: '', + [baseSchema._id]: { + addressSubFields: { + postalCode: '123az#$%', + blockNumber: '', + streetName: '', + buildingName: '', + levelNumber: '', + unitNumber: '', + }, + }, }, } @@ -138,12 +156,16 @@ export const InvalidBlockAndUnit = Template.bind({}) InvalidBlockAndUnit.args = { schema: baseSchema, defaultValue: { - postalCode: '123456', - blockNumber: '%1', - streetName: 'Bukit Batok Street', - buildingName: '50', - levelNumber: '04', - unitNumber: '5A@', + [baseSchema._id]: { + addressSubFields: { + postalCode: '123456', + blockNumber: '%1', + streetName: 'Bukit Batok Street', + buildingName: '50', + levelNumber: '04', + unitNumber: '5A@', + }, + }, }, } @@ -151,12 +173,16 @@ export const InvalidLevelUnit = Template.bind({}) InvalidLevelUnit.args = { schema: baseSchema, defaultValue: { - postalCode: '123456', - blockNumber: '1', - streetName: 'Bukit Batok Street', - buildingName: '50', - levelNumber: '04#', - unitNumber: '5A', + [baseSchema._id]: { + addressSubFields: { + postalCode: '123456', + blockNumber: '1', + streetName: 'Bukit Batok Street', + buildingName: '50', + levelNumber: '04#', + unitNumber: '5A', + }, + }, }, } @@ -164,12 +190,16 @@ export const ValidPostalCodeApiFail = Template.bind({}) ValidPostalCodeApiFail.args = { schema: baseSchema, defaultValue: { - postalCode: '000000', - blockNumber: '', - streetName: '', - buildingName: '', - levelNumber: '', - unitNumber: '', + [baseSchema._id]: { + addressSubFields: { + postalCode: '000000', + blockNumber: '', + streetName: '', + buildingName: '', + levelNumber: '', + unitNumber: '', + }, + }, }, } diff --git a/frontend/src/templates/Field/Address/AddressField.tsx b/frontend/src/templates/Field/Address/AddressField.tsx index d6c163e806..029dc8285f 100644 --- a/frontend/src/templates/Field/Address/AddressField.tsx +++ b/frontend/src/templates/Field/Address/AddressField.tsx @@ -1,7 +1,16 @@ -import { useMemo, useState } from 'react' -import { Controller, useFormContext, useFormState } from 'react-hook-form' +import { useEffect, useMemo, useState } from 'react' +import { + Controller, + FieldArrayWithId, + RegisterOptions, + useFieldArray, + useForm, + useFormContext, + useFormState, +} from 'react-hook-form' import { Box, Flex, FormControl, Stack } from '@chakra-ui/react' +import { AddressAttributes } from '~shared/types' import { VALID_POSTAL_CODE_NO_ADDRESS_ERROR, validatePostalCode, @@ -20,21 +29,24 @@ import Input from '~components/Input' import { verifyAddress } from '../../../../../src/app/services/address/address.service' import { BaseFieldProps } from '../FieldContainer' -import { AddressFieldInput, AddressFieldSchema } from '../types' +import { AddressCompoundFieldInput, AddressCompoundFieldSchema } from '../types' -export interface AddressFieldProps extends BaseFieldProps { - schema: AddressFieldSchema +export interface AddressCompoundFieldProps extends BaseFieldProps { + schema: AddressCompoundFieldSchema disableRequiredValidation?: boolean } -export const AddressField = ({ +export const AddressCompoundField = ({ schema, disableRequiredValidation, -}: AddressFieldProps): JSX.Element => { - const { isSubmitting, isValid, errors } = useFormState() - - const { getValues, setValue, control, trigger, setError } = - useFormContext() +}: AddressCompoundFieldProps): JSX.Element => { + const formContext = useFormContext() + const { getValues, setValue, trigger, setError } = formContext + const { isSubmitting, isValid, errors } = + useFormState({ + name: schema._id, + }) + const addressSubFieldErrors = errors?.[schema._id]?.addressSubFields // only one (first) set of address values const postalCodeValidationRules = useMemo( () => createPostalCodeValidationRules(schema, disableRequiredValidation), @@ -61,29 +73,36 @@ export const AddressField = ({ const handleVerifyAddress = async () => { setIsButtonDisabled(true) - const postalCode = getValues('postalCode') + const postalCode = getValues(`${schema._id}.addressSubFields.postalCode`) if (validatePostalCode(postalCode) !== true) { await trigger(['postalCode']) } else { // Call the service to verify the address const result = await verifyAddress(postalCode) + if (result.success && result.data) { - setValue('blockNumber', result.data?.blockNumber) - setValue('streetName', result.data?.streetName) + setValue( + `${schema._id}.addressSubFields.blockNumber`, + result.data?.blockNumber, + ) + setValue( + `${schema._id}.addressSubFields.streetName`, + result.data?.streetName, + ) await trigger(['blockNumber', 'streetName']) // clear errors if first verification failed } else { if (!result.success) { - setError('blockNumber', { + setError(`${schema._id}.addressSubFields.blockNumber`, { type: 'manual', message: VALID_POSTAL_CODE_NO_ADDRESS_ERROR, }) - setError('streetName', { + setError(`${schema._id}.addressSubFields.streetName`, { type: 'manual', message: VALID_POSTAL_CODE_NO_ADDRESS_ERROR, }) - setValue('blockNumber', '') // reset values if verification failure - setValue('streetName', '') + setValue(`${schema._id}.addressSubFields.blockNumber`, '') // reset values if verification failure + setValue(`${schema._id}.addressSubFields.streetName`, '') await trigger(['postalCode']) // show postalCode error upon verification failure } } @@ -106,117 +125,131 @@ export const AddressField = ({ > {schema.title} + {/** Postal Code */} Postal code ( - - - - - - {errors.postalCode?.message} - - )} + + + + + {addressSubFieldErrors?.postalCode?.message} + + + ) + }} /> + {/** Block Number */} - House/Block number + Block number ( - + - {errors.blockNumber?.message} - + + {addressSubFieldErrors?.blockNumber?.message} + + )} /> + {/** Street Name */} Street name ( - {errors.streetName?.message} + + {addressSubFieldErrors?.streetName?.message} + )} /> + {/** Building name */} Building name ( )} /> + {/** Unit Number & Level Number */} Unit number ( - {errors.levelNumber?.message} + {addressSubFieldErrors?.levelNumber?.message} )} @@ -250,11 +283,11 @@ export const AddressField = ({ isRequired={false} // unitNumber will always be optional isDisabled={schema.disabled} isReadOnly={isValid && isSubmitting} - isInvalid={!!errors?.unitNumber} + isInvalid={!!addressSubFieldErrors?.unitNumber} > ( @@ -265,7 +298,7 @@ export const AddressField = ({ placeholder="Unit number" /> - {errors.unitNumber?.message} + {addressSubFieldErrors?.unitNumber?.message} )} diff --git a/frontend/src/templates/Field/Address/index.ts b/frontend/src/templates/Field/Address/index.ts index ee14fea03f..159ea5ec46 100644 --- a/frontend/src/templates/Field/Address/index.ts +++ b/frontend/src/templates/Field/Address/index.ts @@ -1 +1 @@ -export { AddressField as default } from './AddressField' +export { AddressCompoundField as default } from './AddressField' diff --git a/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx b/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx index ed763a3d32..9616897c6f 100644 --- a/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx +++ b/frontend/src/templates/Field/ChildrenCompound/ChildrenCompoundField.tsx @@ -259,7 +259,7 @@ const ChildrenBody = ({ }, [myInfoChildrenBirthRecords, allChildren, allSelectedNames]) const childNameValues = useMemo(() => { - return [childName, ...namesNotSelected()].filter((name) => { + return [childName, ...namesNotSelected(), 'test'].filter((name) => { if (name === '' || name === undefined) { return false } else return true diff --git a/frontend/src/templates/Field/index.ts b/frontend/src/templates/Field/index.ts index aba8001e14..d9a2beb015 100644 --- a/frontend/src/templates/Field/index.ts +++ b/frontend/src/templates/Field/index.ts @@ -1,4 +1,4 @@ -import AddressField from './Address' +import AddressCompoundField from './Address' import AttachmentField from './Attachment' import CheckboxField from './Checkbox' import ChildrenCompoundField from './ChildrenCompound' @@ -28,7 +28,7 @@ export * from './types' const SectionField = SectionFieldContainer export { - AddressField, + AddressCompoundField, AttachmentField, CheckboxField, ChildrenCompoundField, diff --git a/frontend/src/templates/Field/types.ts b/frontend/src/templates/Field/types.ts index 746a9968e6..5f08d639d5 100644 --- a/frontend/src/templates/Field/types.ts +++ b/frontend/src/templates/Field/types.ts @@ -11,7 +11,8 @@ import { VerifiableResponseBase, } from '~shared/types' import { - AddressFieldBase, + AddressAttributes, + AddressCompoundFieldBase, AttachmentFieldBase, BasicField, CheckboxFieldBase, @@ -62,7 +63,7 @@ export type FormFieldValues = Record< [PAYMENT_PRODUCT_FIELD_ID]?: ProductItemInput[] } -export type AddressFieldInput = AddressFieldValues // specific field-type defined in AddressFieldValues +export type AddressCompoundFieldInput = FieldInput export type AttachmentFieldInput = FieldInput export type CheckboxFieldInputs = FieldInput export type RadioFieldInputs = FieldInput @@ -101,7 +102,7 @@ export type FormFieldValue = F extends : F extends BasicField.Children ? ChildrenCompoundFieldValues : F extends BasicField.Address - ? AddressFieldValues + ? AddressCompoundFieldValues : never // Input values, what each field contains @@ -147,13 +148,8 @@ export type ChildrenCompoundFieldValues = { childFields: MyInfoChildAttributes[] } -export type AddressFieldValues = { - postalCode: string - blockNumber: string // or HouseNumber - streetName: string - buildingName: string - levelNumber: string - unitNumber: string // unit number can be 01-07, or 6A +export type AddressCompoundFieldValues = { + addressSubFields: AddressAttributes } // Various schemas used by different fields @@ -162,7 +158,8 @@ export type ParagraphFieldSchema = FormFieldWithId export type ImageFieldSchema = FormFieldWithId // With question number -export type AddressFieldSchema = FormFieldWithQuestionNo +export type AddressCompoundFieldSchema = + FormFieldWithQuestionNo export type AttachmentFieldSchema = FormFieldWithQuestionNo export type CheckboxFieldSchema = FormFieldWithQuestionNo export type DateFieldSchema = FormFieldWithQuestionNo diff --git a/frontend/src/utils/fieldValidation.ts b/frontend/src/utils/fieldValidation.ts index 1c7136db44..efead264f2 100644 --- a/frontend/src/utils/fieldValidation.ts +++ b/frontend/src/utils/fieldValidation.ts @@ -10,7 +10,7 @@ import validator from 'validator' import { DATE_PARSE_FORMAT } from '~shared/constants/dates' import { - AddressFieldBase, + AddressCompoundFieldBase, AttachmentFieldBase, BasicField, CheckboxFieldBase, @@ -215,7 +215,7 @@ export const createHomeNoValidationRules: ValidationRuleFn = ( } export const createPostalCodeValidationRules: ValidationRuleFn< - AddressFieldBase + AddressCompoundFieldBase > = (schema, disableRequiredValidation): RegisterOptions => { return { validate: { @@ -232,7 +232,7 @@ export const createPostalCodeValidationRules: ValidationRuleFn< } export const createBlockNumberValidationRules: ValidationRuleFn< - AddressFieldBase + AddressCompoundFieldBase > = (schema, disableRequiredValidation): RegisterOptions => { return { validate: { @@ -249,7 +249,7 @@ export const createBlockNumberValidationRules: ValidationRuleFn< } export const createStreetNameValidationRules: ValidationRuleFn< - AddressFieldBase + AddressCompoundFieldBase > = (schema, disableRequiredValidation): RegisterOptions => { return { validate: { @@ -262,7 +262,7 @@ export const createStreetNameValidationRules: ValidationRuleFn< } export const createUnitLevelNumberValidationRules: ValidationRuleFn< - AddressFieldBase + AddressCompoundFieldBase > = (): RegisterOptions => { return { validate: { diff --git a/shared/constants/field/basic.ts b/shared/constants/field/basic.ts index 2bdaf4d044..bdf6063c7e 100644 --- a/shared/constants/field/basic.ts +++ b/shared/constants/field/basic.ts @@ -142,7 +142,7 @@ export const types: BasicFieldBlock[] = [ name: BasicField.Address, value: 'Address', submitted: true, - answerArray: false, + answerArray: true, }, ] diff --git a/shared/types/field/addressField.ts b/shared/types/field/addressField.ts index 998b678e5e..2c815efd0e 100644 --- a/shared/types/field/addressField.ts +++ b/shared/types/field/addressField.ts @@ -1,5 +1,15 @@ import { BasicField, FieldBase } from './base' -export interface AddressFieldBase extends FieldBase { +export interface AddressCompoundFieldBase extends FieldBase { fieldType: BasicField.Address + addressSubFields?: AddressAttributes // 1 address for the field (not allow multiple) +} + +export type AddressAttributes = { + postalCode: string + blockNumber: string + streetName: string + buildingName: string + levelNumber: string + unitNumber: string } diff --git a/shared/types/field/index.ts b/shared/types/field/index.ts index da1abfdab8..41a2817142 100644 --- a/shared/types/field/index.ts +++ b/shared/types/field/index.ts @@ -1,4 +1,4 @@ -import type { AddressFieldBase } from './addressField' +import type { AddressCompoundFieldBase } from './addressField' import type { AttachmentFieldBase } from './attachmentField' import type { CheckboxFieldBase } from './checkboxField' import type { CountryRegionFieldBase } from './countryRegionField' @@ -51,7 +51,7 @@ export * from './yesNoField' export * from './childrenCompoundField' export type FormField = - | AddressFieldBase + | AddressCompoundFieldBase | AttachmentFieldBase | CheckboxFieldBase | DateFieldBase diff --git a/shared/types/response-v3.ts b/shared/types/response-v3.ts index c2a3d22950..08bfc1cbd5 100644 --- a/shared/types/response-v3.ts +++ b/shared/types/response-v3.ts @@ -1,4 +1,9 @@ -import { BasicField, FormFieldDto, MyInfoChildAttributes } from './field' +import { + AddressAttributes, + BasicField, + FormFieldDto, + MyInfoChildAttributes, +} from './field' export type FieldResponsesV3 = Record @@ -79,7 +84,7 @@ export type FieldResponseAnswerMapV3 = : F extends BasicField.Children ? ChildrenCompoundFieldResponsesV3 : F extends BasicField.Address - ? AddressFieldResponseV3 + ? AddressCompoundFieldResponseV3 : never export type GenericStringAnswerResponseFieldTypeV3 = @@ -126,11 +131,6 @@ export type AttachmentFieldResponseV3 = { md5Hash?: string } -export type AddressFieldResponseV3 = { - postalCode: string - blockNumber: string - streetName: string - buildingName: string - levelNumber: string - unitNumber: string +export type AddressCompoundFieldResponseV3 = { + addressSubFields: AddressAttributes } diff --git a/shared/types/response.ts b/shared/types/response.ts index fce1b74483..aec923f614 100644 --- a/shared/types/response.ts +++ b/shared/types/response.ts @@ -1,6 +1,6 @@ import type { Opaque } from 'type-fest' import { z } from 'zod' -import { BasicField, MyInfoAttribute } from './field' +import { AddressAttributes, BasicField, MyInfoAttribute } from './field' const ResponseBase = z.object({ myInfo: z.never().optional(), @@ -132,9 +132,10 @@ export const UenResponse = SingleAnswerResponse.extend({ }) export type UenResponse = z.infer -// TODO: check fo multivalue response -export const AddressResponse = SingleAnswerResponse.extend({ +// TODO: check fo multivalue response as a string[] +export const AddressResponse = ResponseBase.extend({ fieldType: z.literal(BasicField.Address), + answerArray: z.array(z.array(z.string())) as unknown as z.Schema, }) export type AddressResponse = z.infer diff --git a/src/app/models/field/addressField.ts b/src/app/models/field/addressField.ts index 11a117eb4c..94bdaa2d7a 100644 --- a/src/app/models/field/addressField.ts +++ b/src/app/models/field/addressField.ts @@ -1,9 +1,9 @@ import { Schema } from 'mongoose' -import { IAddressFieldSchema } from 'src/types' +import { IAddressCompoundFieldSchema } from 'src/types' -const createAddressFieldSchema = () => { - return new Schema({}) +const createAddressCompoundFieldSchema = () => { + return new Schema({}) } -export default createAddressFieldSchema +export default createAddressCompoundFieldSchema diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts index 8e35db506c..c768ffc369 100644 --- a/src/app/modules/form/form.service.ts +++ b/src/app/modules/form/form.service.ts @@ -631,11 +631,11 @@ export const createSingleSampleSubmissionAnswer = (field: FormFieldDto) => { } } case BasicField.Address: { - const sampleValue = faker.lorem.words() + const sampleValue: string[] = [] return { id: field._id, question: field.title, - answer: sampleValue, + answerArray: sampleValue, fieldType: field.fieldType, } } diff --git a/src/app/modules/submission/email-submission/email-submission.util.ts b/src/app/modules/submission/email-submission/email-submission.util.ts index 75cb69445a..47c9703601 100644 --- a/src/app/modules/submission/email-submission/email-submission.util.ts +++ b/src/app/modules/submission/email-submission/email-submission.util.ts @@ -28,6 +28,7 @@ import { VerifyTurnstileError, } from '../../../services/turnstile/turnstile.errors' import { + isProcessedAddressResponse, isProcessedCheckboxResponse, isProcessedChildResponse, isProcessedTableResponse, @@ -73,6 +74,7 @@ import { ValidateFieldError, } from '../submission.errors' import { + ProcessedAddressResponse, ProcessedCheckboxResponse, ProcessedFieldResponse, ProcessedTableResponse, @@ -191,6 +193,27 @@ export const getAnswerForCheckbox = ( } } +/** + * Creates a response for address, with its answer formatted from the answerArray + * @param response + * @param response.answerArray is of type AddressAttributes + * @returns the response with formatted answer + */ +export const getAnswerForAddress = ( + response: ProcessedAddressResponse, +): ResponseFormattedForEmail => { + return { + _id: response._id, + fieldType: response.fieldType, + question: response.question, + myInfo: response.myInfo, + isVisible: response.isVisible, + isUserVerified: response.isUserVerified, + answer: JSON.stringify(response.answerArray), + } +} + + /** * Formats the response for sending to the submitter (autoReplyData), * the table that is sent to the admin (formData), @@ -442,6 +465,9 @@ const createFormattedDataForOneField = ( return getAnswersForChild(response).map((childField) => getFormattedFunction(childField, hashedFields), ) + } else if (isProcessedAddressResponse(response)) { + const address = getAnswerForAddress(response) + return [getFormattedFunction(address, hashedFields)] } else { return [getFormattedFunction(response, hashedFields)] } diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts index dd61ba8c5c..05378c0ddd 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts @@ -219,8 +219,8 @@ export const getQuestionTitleAnswerString = ({ answer, }) continue - case BasicField.Address: // TODO - answer = response.answer.postalCode + case BasicField.Address: + answer = response.answer.addressSubFields.postalCode // TODO break case BasicField.Email: case BasicField.Mobile: diff --git a/src/app/modules/submission/submission.types.ts b/src/app/modules/submission/submission.types.ts index 3685ba2777..7cbb47e4f6 100644 --- a/src/app/modules/submission/submission.types.ts +++ b/src/app/modules/submission/submission.types.ts @@ -2,6 +2,8 @@ import { StatusCodes } from 'http-status-codes' import type { Opaque } from 'type-fest' import { + AddressAttributes, + AddressResponse, BasicField, CheckboxResponse, ChildBirthRecordsResponse, @@ -69,6 +71,7 @@ export type ProcessedChildrenResponse = ChildBirthRecordsResponse & childSubFieldsArray?: MyInfoChildAttributes[] childIdx?: number } +export type ProcessedAddressResponse = AddressResponse & ProcessedResponse /** * Can be either email or storage mode attachment response. * Email mode attachment response in the server will have extra metadata injected @@ -87,6 +90,7 @@ export type ProcessedFieldResponse = | ProcessedTableResponse | ProcessedAttachmentResponse | ProcessedChildrenResponse + | ProcessedAddressResponse /** * Virus scanner types diff --git a/src/app/utils/field-validation/answerValidator.factory.ts b/src/app/utils/field-validation/answerValidator.factory.ts index 47192e5463..c3c0c4e3d1 100644 --- a/src/app/utils/field-validation/answerValidator.factory.ts +++ b/src/app/utils/field-validation/answerValidator.factory.ts @@ -5,6 +5,7 @@ import { FieldValidationSchema } from '../../../types' import { ParsedClearFormFieldResponseV3 } from '../../../types/api' import { ResponseValidator } from '../../../types/field/utils/validation' import { + ProcessedAddressResponse, ProcessedAttachmentResponse, ProcessedCheckboxResponse, ProcessedChildrenResponse, @@ -100,8 +101,6 @@ export const constructSingleAnswerValidator = ( formField: FieldValidationSchema, ): ResponseValidator => { switch (formField.fieldType) { - case BasicField.Address: - return constructAddressValidator(formField) case BasicField.Section: return constructSectionValidator() case BasicField.ShortText: @@ -176,6 +175,15 @@ export const constructTableFieldValidator = ( return () => left('Unsupported field type') } +export const constructAddressFieldValidator = ( + formField: FieldValidationSchema, +): ResponseValidator => { + if (formField.fieldType === BasicField.Address) { + return constructAddressValidator(formField) + } + return () => left('Unsupported field type') +} + export const constructFieldResponseValidatorV3 = ({ formId, formField, diff --git a/src/app/utils/field-validation/field-validation.guards.ts b/src/app/utils/field-validation/field-validation.guards.ts index cee6b54646..6e7e16b7b8 100644 --- a/src/app/utils/field-validation/field-validation.guards.ts +++ b/src/app/utils/field-validation/field-validation.guards.ts @@ -11,6 +11,7 @@ import { import { IEmailFieldSchema } from '../../../types' import { ColumnResponse, + ProcessedAddressResponse, ProcessedAttachmentResponse, ProcessedCheckboxResponse, ProcessedChildrenResponse, @@ -55,6 +56,19 @@ export const isProcessedChildResponse = ( ) } +export const isProcessedAddressResponse = ( + response: ProcessedFieldResponse, +): response is ProcessedAddressResponse => { + return ( + response.fieldType === BasicField.Address && + 'answerArray' in response && + Array.isArray(response.answerArray) && + response.answerArray.every( + (item) => typeof item === 'object' && item !== null, // Assuming AddressAttribute is an object + ) + ) +} + const isStringArray = (arr: unknown): arr is string[] => Array.isArray(arr) && arr.every((item) => typeof item === 'string') diff --git a/src/app/utils/field-validation/validators/addressValidator.ts b/src/app/utils/field-validation/validators/addressValidator.ts index 6b4b77b8d0..9d28d00945 100644 --- a/src/app/utils/field-validation/validators/addressValidator.ts +++ b/src/app/utils/field-validation/validators/addressValidator.ts @@ -1,10 +1,18 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' -import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' -import { IAddressFieldSchema, OmitUnusedValidatorProps } from 'src/types' +import { ProcessedAddressResponse } from 'src/app/modules/submission/submission.types' +import { + IAddressCompoundFieldSchema, + OmitUnusedValidatorProps, +} from 'src/types' -import { AddressResponseV3, BasicField } from '../../../../../shared/types' +import { + AddressCompoundFieldBase, + AddressResponse, + AddressResponseV3, + BasicField, +} from '../../../../../shared/types' import { validatePostalCode } from '../../../../../shared/utils/address-validation' import { ParsedClearFormFieldResponseV3 } from '../../../../types/api' import { @@ -12,29 +20,31 @@ import { ResponseValidatorConstructor, } from '../../../../types/field/utils/validation' -import { - notEmptySingleAnswerResponse, - // notEmptySingleAnswerResponseV3, -} from './common' - -type AddressValidator = ResponseValidator +type AddressValidator = ResponseValidator type AddressValidatorConstructor = ( - addressField: OmitUnusedValidatorProps, + addressField: OmitUnusedValidatorProps, ) => AddressValidator /** * Returns a validator to check if postal code format is correct */ const addressValidator: AddressValidator = (response) => { - return validatePostalCode(response.answer) + const { answerArray } = response + return validatePostalCode(answerArray.postalCode) ? right(response) : left(`AddressValidator:\t answer is not a valid postal code`) } export const constructAddressValidator: AddressValidatorConstructor = () => - flow(notEmptySingleAnswerResponse, chain(addressValidator)) + flow(addressValidator) // TODO fix this // v3 +// interface AddressValidatorData { +// addressField: AddressCompoundFieldBase +// formId: string +// isVisible: boolean +// isDisabled: boolean +// } const isAddressResponseV3: ResponseValidator< ParsedClearFormFieldResponseV3, @@ -48,19 +58,25 @@ const isAddressResponseV3: ResponseValidator< return right(response) } -const addressValidatorV3: ResponseValidator = (response) => { - return validatePostalCode(response.answer.postalCode) - ? right(response) - : left(`AddressValidatorV3:\t answer is not a valid postal code`) -} +// const addressValidatorV3: ResponseValidator< +// AddressValidatorData, +// AddressResponseV3 +// > = +// ({ addressField }) => +// (response) => { +// const answerArray = response.answerArray +// return validatePostalCode(response.answer.postalCode) +// ? right(response) +// : left(`AddressValidatorV3:\t answer is not a valid postal code`) +// } export const constructAddressValidatorV3: ResponseValidatorConstructor< - OmitUnusedValidatorProps, + OmitUnusedValidatorProps, ParsedClearFormFieldResponseV3, AddressResponseV3 > = () => flow( isAddressResponseV3, // chain(notEmptySingleAnswerResponseV3), - chain(addressValidatorV3), + // chain(addressValidatorV3), // TODO fix this ) diff --git a/src/types/field/addressField.ts b/src/types/field/addressField.ts index 1441e9083b..f45bb161ef 100644 --- a/src/types/field/addressField.ts +++ b/src/types/field/addressField.ts @@ -1,7 +1,9 @@ -import { AddressFieldBase, BasicField } from '../../../shared/types' +import { AddressCompoundFieldBase, BasicField } from '../../../shared/types' import { IFieldSchema } from './baseField' -export interface IAddressFieldSchema extends AddressFieldBase, IFieldSchema { +export interface IAddressCompoundFieldSchema + extends AddressCompoundFieldBase, + IFieldSchema { fieldType: BasicField.Address } diff --git a/src/types/field/index.ts b/src/types/field/index.ts index 70195f34b5..ad1c17acc8 100644 --- a/src/types/field/index.ts +++ b/src/types/field/index.ts @@ -1,7 +1,7 @@ import type { Document } from 'mongoose' import type { ConditionalExcept, Merge } from 'type-fest' -import type { IAddressFieldSchema } from './addressField' +import type { IAddressCompoundFieldSchema } from './addressField' import type { IAttachmentFieldSchema } from './attachmentField' import type { ICheckboxFieldSchema } from './checkboxField' import type { IChildrenCompoundFieldSchema } from './childrenCompoundField' @@ -62,7 +62,7 @@ export enum SgidFieldTitle { } export type FormFieldSchema = - | IAddressFieldSchema + | IAddressCompoundFieldSchema | IAttachmentFieldSchema | ICheckboxFieldSchema | IDateFieldSchema @@ -105,7 +105,7 @@ export type OmitUnusedValidatorProps = Merge< > export type FieldValidationSchema = - | OmitUnusedValidatorProps + | OmitUnusedValidatorProps | OmitUnusedValidatorProps | OmitUnusedValidatorProps | OmitUnusedValidatorProps diff --git a/src/types/response/index.ts b/src/types/response/index.ts index 4d6f110d0f..2f91156dfd 100644 --- a/src/types/response/index.ts +++ b/src/types/response/index.ts @@ -1,4 +1,5 @@ import { + AddressResponse, CheckboxResponse, ChildBirthRecordsResponse, TableResponse, @@ -21,6 +22,7 @@ export type SingleAnswerFieldResponse = | CheckboxResponse | IAttachmentResponse | ChildBirthRecordsResponse + | AddressResponse > | Exclude< ParsedClearFormFieldResponse, @@ -28,6 +30,7 @@ export type SingleAnswerFieldResponse = | CheckboxResponse | IAttachmentResponse | ChildBirthRecordsResponse + | AddressResponse > export type FieldResponse =