Skip to content

Commit

Permalink
feat: add Country/Region field (#5132)
Browse files Browse the repository at this point in the history
* feat: add country field backend (#3770)

* feat: add country field backend

* fix: Fix Property 'country' is missing

* fix: replace BasicField.Dropdown in CountryField story

* fix: import CountryField into FieldFactory

* fix: fix type errors due to lack of handling for new country field

Co-authored-by: Kar Rui <[email protected]>

* feat: add CountryRegion field to frontend (#4112)

* feat: add country field backend (#3770)

* feat: add country field backend

* fix: Fix Property 'country' is missing

* fix: replace BasicField.Dropdown in CountryField story

* fix: import CountryField into FieldFactory

* fix: fix type errors due to lack of handling for new country field

Co-authored-by: Kar Rui <[email protected]>

* feat: add Country field to frontend

* feat: move Singapore up to the first value in the Country enum

* test: add tests for CountryField

* fix: update the typed characters in the partial typing test case

* test: adjust test case for partial typing in Country field

* refactor: rename Country enum to CountryRegion

* refactor: rename BasicField.Country to BasicField.CountryRegion

* refactor: rename CountryField to CountryRegionField

* fix: commit renaming changes for tests/

* refactor: rename *Country* to *CountryRegion* in frontend edit field folder

* feat: sort CountryRegion enums inside CountryRegionField

* fix: remove removed isSaveEnabled prop

* fix: inline sort country options constant

* test: update CountryRegionField tests

* fix(CountryRegionField): correctly set singapore as the first option

also update tests to use the options that the field uses

* style: use flag icon and fix capitalisation for CountryRegion

* feat: allow validation rule overriding for DropdownField

* fix: update error message for invalid Country/Region

* test: fix country/region capitalisation in test

Co-authored-by: Kar Rui <[email protected]>

* fix: remove duplicate country files

* fix: remove duplicate country field files and restore TODO

* chore: rename test file

* fix: add missing imports

* fix: use new CreatePageDrawerContentContainer component

* fix: dropdown now uses requiredSingleAnswerValidationFn for validation

* fix: add validation and processing of CountryRegion fields on client

* refactor: implement specific CRField instead of using DropdownField

* fix: remove unused field schema regression

* feat: make CountryRegion values uppercase when sent to the server

* Revert "feat: make CountryRegion values uppercase when sent to the server"

This reverts commit be8a9d3.

* fix: use title-case country/region only in render

* Revert "fix: use title-case country/region only in render"

This reverts commit 589af79.

* feat: transform Country/Region form inputs to upper-case on submit

* fix: validate against upper-case country/region, add comments on why this is done, and remove reference to any

* fix: remove reference to toString()

* fix: simulate transformations from handleSubmitForm in country-region-validation.spec.ts

* fix: resolve rebase and use === instead of ==

* fix: remove duplicate import

* chore: fix prettier violations

* fix: update generate-form-data import path

* fix: follow DropdownField's way of querying input element and casting options

* fix: update INVALID_COUNTRY_REGION_OPTION_ERROR

* fix: linting

* fix: use uppercased countryregion for non-fetch submission

---------

Co-authored-by: Kar Rui <[email protected]>
Co-authored-by: Jiayee Lim <[email protected]>
Co-authored-by: Jiayee <[email protected]>
Co-authored-by: tshuli <[email protected]>
  • Loading branch information
5 people authored Aug 2, 2023
1 parent cfa0544 commit 6cda531
Show file tree
Hide file tree
Showing 39 changed files with 943 additions and 320 deletions.
7 changes: 7 additions & 0 deletions __tests__/unit/backend/helpers/generate-form-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
IAttachmentFieldSchema,
IAttachmentResponse,
ICheckboxFieldSchema,
ICountryRegionFieldSchema,
IDateFieldSchema,
IDecimalFieldSchema,
IDropdownFieldSchema,
Expand Down Expand Up @@ -117,6 +118,12 @@ export const generateDefaultField = (
getQuestion: () => defaultParams.title,
...customParams,
} as IDropdownFieldSchema
case BasicField.CountryRegion:
return {
...defaultParams,
getQuestion: () => defaultParams.title,
...customParams,
} as ICountryRegionFieldSchema
case BasicField.Decimal:
return {
...defaultParams,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/constants/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export const INVALID_EMAIL_DOMAIN_ERROR =

export const INVALID_DROPDOWN_OPTION_ERROR =
'Entered value is not a valid dropdown option'

export const INVALID_COUNTRY_REGION_OPTION_ERROR =
'Please select a valid country/region from the dropdown list'
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
AttachmentField,
CheckboxField,
ChildrenCompoundField,
CountryRegionField,
DateField,
DecimalField,
DropdownField,
Expand Down Expand Up @@ -482,6 +483,8 @@ const FieldRow = ({ field, ...rest }: FieldRowProps) => {
return <DateField schema={field} {...rest} />
case BasicField.Dropdown:
return <DropdownField schema={field} {...rest} />
case BasicField.CountryRegion:
return <CountryRegionField schema={field} {...rest} />
case BasicField.ShortText:
return <ShortTextField schema={field} {...rest} />
case BasicField.LongText:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import {
EditAttachment,
EditCheckbox,
EditCountryRegion,
EditDate,
EditDecimal,
EditDropdown,
Expand Down Expand Up @@ -130,6 +131,8 @@ export const MemoFieldDrawerContent = memo<MemoFieldDrawerContentProps>(
return <EditCheckbox {...props} field={field} />
case BasicField.Dropdown:
return <EditDropdown {...props} field={field} />
case BasicField.CountryRegion:
return <EditCountryRegion {...props} field={field} />
case BasicField.Mobile:
return <EditMobile {...props} field={field} />
case BasicField.HomeNo:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Meta, Story } from '@storybook/react'

import { CountryRegion } from '~shared/constants/countryRegion'
import { BasicField, CountryRegionFieldBase } from '~shared/types'

import { createFormBuilderMocks } from '~/mocks/msw/handlers/admin-form'

import { EditFieldDrawerDecorator, StoryRouter } from '~utils/storybook'

import { EditCountryRegion } from './EditCountryRegion'

const DEFAULT_COUNTRY_REGION_FIELD: CountryRegionFieldBase = {
title: 'Storybook Country/Region',
description: 'Some description about Country/Region',
required: true,
disabled: false,
fieldType: BasicField.CountryRegion,
fieldOptions: Object.values(CountryRegion),
globalId: 'unused',
}

export default {
title: 'Features/AdminForm/EditFieldDrawer/EditCountryRegion',
component: EditCountryRegion,
decorators: [
StoryRouter({
initialEntries: ['/61540ece3d4a6e50ac0cc6ff'],
path: '/:formId',
}),
EditFieldDrawerDecorator,
],
parameters: {
// Required so skeleton "animation" does not hide content.
chromatic: { pauseAnimationAtEnd: true },
msw: createFormBuilderMocks({}, 0),
},
args: {
field: DEFAULT_COUNTRY_REGION_FIELD,
},
} as Meta<StoryArgs>

interface StoryArgs {
field: CountryRegionFieldBase
}

const Template: Story<StoryArgs> = ({ field }) => {
return <EditCountryRegion field={field} />
}

export const Default = Template.bind({})
Default.storyName = 'EditCountryRegion'
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useMemo } from 'react'
import { FormControl } from '@chakra-ui/react'
import { extend, pick } from 'lodash'

import { CountryRegionFieldBase } from '~shared/types/field'

import { createBaseValidationRules } from '~utils/fieldValidation'
import FormErrorMessage from '~components/FormControl/FormErrorMessage'
import FormLabel from '~components/FormControl/FormLabel'
import Input from '~components/Input'
import Textarea from '~components/Textarea'
import Toggle from '~components/Toggle'

import { CreatePageDrawerContentContainer } from '~features/admin-form/create/common'

import { FormFieldDrawerActions } from '../common/FormFieldDrawerActions'
import { EditFieldProps } from '../common/types'
import { useEditFieldForm } from '../common/useEditFieldForm'

type EditCountryRegionProps = EditFieldProps<CountryRegionFieldBase>

const EDIT_COUNTRY_FIELD_KEYS = ['title', 'description', 'required'] as const

type EditCountryRegionKeys = typeof EDIT_COUNTRY_FIELD_KEYS[number]

type EditCountryRegionInputs = Pick<
CountryRegionFieldBase,
EditCountryRegionKeys
>

const transformCountryRegionFieldToEditForm = (
field: CountryRegionFieldBase,
): EditCountryRegionInputs => {
return {
...pick(field, EDIT_COUNTRY_FIELD_KEYS),
}
}

const transformCountryRegionEditFormToField = (
inputs: EditCountryRegionInputs,
originalField: CountryRegionFieldBase,
): CountryRegionFieldBase => {
return extend({}, originalField, inputs, {})
}

export const EditCountryRegion = ({
field,
}: EditCountryRegionProps): JSX.Element => {
const {
register,
formState: { errors },
buttonText,
handleUpdateField,
isLoading,
handleCancel,
} = useEditFieldForm<EditCountryRegionInputs, CountryRegionFieldBase>({
field,
transform: {
input: transformCountryRegionFieldToEditForm,
output: transformCountryRegionEditFormToField,
},
})

const requiredValidationRule = useMemo(
() => createBaseValidationRules({ required: true }),
[],
)

return (
<CreatePageDrawerContentContainer>
<FormControl isRequired isReadOnly={isLoading} isInvalid={!!errors.title}>
<FormLabel>Question</FormLabel>
<Input autoFocus {...register('title', requiredValidationRule)} />
<FormErrorMessage>{errors?.title?.message}</FormErrorMessage>
</FormControl>
<FormControl isReadOnly={isLoading} isInvalid={!!errors.description}>
<FormLabel>Description</FormLabel>
<Textarea {...register('description')} />
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
</FormControl>
<FormControl isReadOnly={isLoading}>
<Toggle {...register('required')} label="Required" />
</FormControl>
<FormFieldDrawerActions
isLoading={isLoading}
buttonText={buttonText}
handleClick={handleUpdateField}
handleCancel={handleCancel}
/>
</CreatePageDrawerContentContainer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EditCountryRegion } from './EditCountryRegion'
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './EditAttachment'
export * from './EditCheckbox'
export * from './EditCountryRegion'
export * from './EditDate'
export * from './EditDecimal'
export * from './EditDropdown'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const BASIC_FIELDS_ORDERED = [
BasicField.Radio,
BasicField.Checkbox,
BasicField.Dropdown,
BasicField.CountryRegion,
BasicField.Section,
BasicField.Statement,
BasicField.YesNo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ export const getFieldCreationMeta = (fieldType: BasicField): FieldCreateDto => {
minimumRows: 2,
}
}
case BasicField.CountryRegion: {
return {
fieldType,
...baseMeta,
fieldOptions: [],
}
}
case BasicField.Children: {
return {
fieldType,
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/features/admin-form/create/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ export const BASICFIELD_TO_DRAWER_META: {
isSubmitted: true,
},

[BasicField.CountryRegion]: {
label: 'Country/Region',
icon: BiFlag,
isSubmitted: true,
},

[BasicField.Email]: {
label: 'Email',
icon: BiMailSend,
Expand Down
42 changes: 39 additions & 3 deletions frontend/src/features/public-form/PublicFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
PAYMENT_CONTACT_FIELD_ID,
PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID,
} from '~shared/constants'
import { PaymentType } from '~shared/types'
import { BasicField, PaymentType } from '~shared/types'
import { CaptchaTypes } from '~shared/types/captcha'
import {
FormAuthType,
Expand Down Expand Up @@ -306,6 +306,28 @@ export const PublicFormProvider = ({
},
}

const countryRegionFieldIds = new Set(
form.form_fields
.filter((field) => field.fieldType === BasicField.CountryRegion)
.map((field) => field._id),
)
// We want users to see the country/region options in title-case but we also need the data in the backend to remain in upper-case.
// Country/region data in the backend needs to remain in upper-case so that they remain consistent with myinfo-countries.
const formInputsWithCountryRegionInUpperCase = Object.keys(
formInputs,
).reduce((newFormInputs: typeof formInputs, fieldId) => {
const currentInput = formInputs[fieldId]
if (
countryRegionFieldIds.has(fieldId) &&
typeof currentInput === 'string'
) {
newFormInputs[fieldId] = currentInput.toUpperCase()
} else {
newFormInputs[fieldId] = currentInput
}
return newFormInputs
}, {})

const logMeta = {
action: 'handleSubmitForm',
useFetchForSubmissions,
Expand Down Expand Up @@ -339,7 +361,13 @@ export const PublicFormProvider = ({
})

return submitEmailModeFormFetchMutation
.mutateAsync(formData, { onSuccess })
.mutateAsync(
{
...formData,
formInputs: formInputsWithCountryRegionInUpperCase,
},
{ onSuccess },
)
.catch(async (error) => {
datadogLogs.logger.warn(`handleSubmitForm: ${error.message}`, {
meta: {
Expand Down Expand Up @@ -371,7 +399,13 @@ export const PublicFormProvider = ({

return (
submitEmailModeFormMutation
.mutateAsync(formData, { onSuccess })
.mutateAsync(
{
...formData,
formInputs: formInputsWithCountryRegionInUpperCase,
},
{ onSuccess },
)
// Using catch since we are using mutateAsync and react-hook-form will continue bubbling this up.
.catch(async (error) => {
// TODO(#5826): Remove when we have resolved the Network Error
Expand Down Expand Up @@ -415,6 +449,7 @@ export const PublicFormProvider = ({
.mutateAsync(
{
...formData,
formInputs: formInputsWithCountryRegionInUpperCase,
publicKey: form.publicKey,
captchaResponse,
captchaType,
Expand Down Expand Up @@ -484,6 +519,7 @@ export const PublicFormProvider = ({
.mutateAsync(
{
...formData,
formInputs: formInputsWithCountryRegionInUpperCase,
publicKey: form.publicKey,
captchaResponse,
captchaType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AttachmentField,
CheckboxField,
ChildrenCompoundField,
CountryRegionField,
DateField,
DecimalField,
DropdownField,
Expand Down Expand Up @@ -70,6 +71,8 @@ export const FieldFactory = memo(
return <YesNoField schema={field} {...rest} />
case BasicField.Dropdown:
return <DropdownField schema={field} {...rest} />
case BasicField.CountryRegion:
return <CountryRegionField schema={field} {...rest} />
case BasicField.Date:
return <DateField schema={field} {...rest} />
case BasicField.Uen:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const createResponsesArray = (
): FieldResponse[] => {
const transformedResponses = formFields
.map((ff) => transformInputsToOutputs(ff, formInputs[ff._id]))
.filter((output): output is FieldResponse => output !== undefined)
.filter((output): output is FieldResponse => output !== null)

return validateResponses(transformedResponses)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,12 @@ const transformToChildOutput = (
* Transforms form inputs to their desire output shapes for sending to the server
* @param field schema to retrieve base field info
* @param input the input corresponding to the field in the form
* @returns If field type does not need an output, `undefined` is returned. Otherwise returns the transformed output.
* @returns If field type does not need an output, `null` is returned. Otherwise returns the transformed output.
*/
export const transformInputsToOutputs = (
field: FormFieldDto,
input: FormFieldValue,
): FieldResponse | undefined => {
): FieldResponse | null => {
switch (field.fieldType) {
case BasicField.Section:
return transformToSectionOutput(field)
Expand Down Expand Up @@ -250,6 +250,7 @@ export const transformInputsToOutputs = (
case BasicField.LongText:
case BasicField.HomeNo:
case BasicField.Dropdown:
case BasicField.CountryRegion:
case BasicField.Rating:
case BasicField.Nric:
case BasicField.Uen:
Expand All @@ -260,7 +261,7 @@ export const transformInputsToOutputs = (
case BasicField.Statement:
case BasicField.Image:
// No output needed.
return undefined
return null
case BasicField.Children:
return transformToChildOutput(
field,
Expand Down
Loading

0 comments on commit 6cda531

Please sign in to comment.