Skip to content

Commit

Permalink
feat(application-system): Make required a dynamic field (#16691)
Browse files Browse the repository at this point in the history
* feat(application-system): Make required a dynamic field

* Added tests

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
norda-gunni and kodiakhq[bot] authored Nov 1, 2024
1 parent 63a12a7 commit dc89848
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 33 deletions.
72 changes: 72 additions & 0 deletions libs/application/core/src/lib/fieldBuilders.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Field } from '@island.is/application/types'

import { FieldComponents, FieldTypes } from '@island.is/application/types'

import { Application } from '@island.is/application/types'
import { buildFieldOptions, buildFieldRequired } from './fieldBuilders'

describe('buildFieldOptions', () => {
const mockApplication = {
id: 'test-app',
state: 'draft',
answers: {},
} as Application

const mockField = {
id: 'test-field',
type: FieldTypes.SELECT,
component: FieldComponents.SELECT,
} as Field

it('should return options array when passed static options', () => {
const staticOptions = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
]

const result = buildFieldOptions(staticOptions, mockApplication, mockField)

expect(result).toEqual(staticOptions)
})

it('should call function with application and field when passed function', () => {
const dynamicOptions = jest.fn().mockReturnValue([
{ label: 'Dynamic 1', value: 'd1' },
{ label: 'Dynamic 2', value: 'd2' },
])

const result = buildFieldOptions(dynamicOptions, mockApplication, mockField)

expect(dynamicOptions).toHaveBeenCalledWith(mockApplication, mockField)
expect(result).toEqual([
{ label: 'Dynamic 1', value: 'd1' },
{ label: 'Dynamic 2', value: 'd2' },
])
})
})

describe('buildFieldRequired', () => {
const mockApplication = {
id: 'test-app',
state: 'draft',
answers: {},
} as Application

it('should return boolean value when passed static boolean', () => {
expect(buildFieldRequired(mockApplication, true)).toBe(true)
expect(buildFieldRequired(mockApplication, false)).toBe(false)
})

it('should return undefined when passed undefined', () => {
expect(buildFieldRequired(mockApplication, undefined)).toBeUndefined()
})

it('should call function with application when passed function', () => {
const dynamicRequired = jest.fn().mockReturnValue(true)

const result = buildFieldRequired(mockApplication, dynamicRequired)

expect(dynamicRequired).toHaveBeenCalledWith(mockApplication)
expect(result).toBe(true)
})
})
12 changes: 11 additions & 1 deletion libs/application/core/src/lib/fieldBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
AccordionField,
BankAccountField,
SliderField,
MaybeWithApplication,
} from '@island.is/application/types'

import { Colors } from '@island.is/island-ui/theme'
Expand Down Expand Up @@ -532,10 +533,19 @@ export const buildFieldOptions = (
if (typeof maybeOptions === 'function') {
return maybeOptions(application, field)
}

return maybeOptions
}

export const buildFieldRequired = (
application: Application,
maybeRequired?: MaybeWithApplication<boolean>,
) => {
if (typeof maybeRequired === 'function') {
return maybeRequired(application)
}
return maybeRequired
}

export const buildRedirectToServicePortalField = (data: {
id: string
title: FormText
Expand Down
36 changes: 16 additions & 20 deletions libs/application/types/src/lib/Fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Locale } from '@island.is/shared/types'
type Space = keyof typeof theme.spacing

export type RecordObject<T = unknown> = Record<string, T>
export type MaybeWithApplication<T> = T | ((application: Application) => T)
export type MaybeWithApplicationAndField<T> =
| T
| ((application: Application, field: Field) => T)
Expand Down Expand Up @@ -196,10 +197,15 @@ export interface BaseField extends FormItem {
isPartOfRepeater?: boolean
defaultValue?: MaybeWithApplicationAndField<unknown>
doesNotRequireAnswer?: boolean

// TODO use something like this for non-schema validation?
// validate?: (formValue: FormValue, context?: object) => boolean
}

export interface InputField extends BaseField {
required?: MaybeWithApplication<boolean>
}

export enum FieldTypes {
CHECKBOX = 'CHECKBOX',
CUSTOM = 'CUSTOM',
Expand Down Expand Up @@ -271,19 +277,18 @@ export enum FieldComponents {
SLIDER = 'SliderFormField',
}

export interface CheckboxField extends BaseField {
export interface CheckboxField extends InputField {
readonly type: FieldTypes.CHECKBOX
component: FieldComponents.CHECKBOX
options: MaybeWithApplicationAndField<Option[]>
large?: boolean
strong?: boolean
required?: boolean
backgroundColor?: InputBackgroundColor
onSelect?: ((s: string[]) => void) | undefined
spacing?: 0 | 1 | 2
}

export interface DateField extends BaseField {
export interface DateField extends InputField {
readonly type: FieldTypes.DATE
placeholder?: FormText
component: FieldComponents.DATE
Expand All @@ -292,7 +297,6 @@ export interface DateField extends BaseField {
excludeDates?: MaybeWithApplicationAndField<Date[]>
backgroundColor?: DatePickerBackgroundColor
onChange?(date: string): void
required?: boolean
readOnly?: boolean
}

Expand All @@ -308,41 +312,38 @@ export interface DescriptionField extends BaseField {
titleVariant?: TitleVariants
}

export interface RadioField extends BaseField {
export interface RadioField extends InputField {
readonly type: FieldTypes.RADIO
component: FieldComponents.RADIO
options: MaybeWithApplicationAndField<Option[]>
backgroundColor?: InputBackgroundColor
largeButtons?: boolean
required?: boolean
space?: BoxProps['paddingTop']
hasIllustration?: boolean
widthWithIllustration?: '1/1' | '1/2' | '1/3'
onSelect?(s: string): void
}

export interface SelectField extends BaseField {
export interface SelectField extends InputField {
readonly type: FieldTypes.SELECT
component: FieldComponents.SELECT
options: MaybeWithApplicationAndField<Option[]>
onSelect?(s: SelectOption, cb: (t: unknown) => void): void
placeholder?: FormText
backgroundColor?: InputBackgroundColor
required?: boolean
isMulti?: boolean
}

export interface CompanySearchField extends BaseField {
export interface CompanySearchField extends InputField {
readonly type: FieldTypes.COMPANY_SEARCH
component: FieldComponents.COMPANY_SEARCH
placeholder?: FormText
setLabelToDataSchema?: boolean
shouldIncludeIsatNumber?: boolean
checkIfEmployerIsOnForbiddenList?: boolean
required?: boolean
}

export interface AsyncSelectField extends BaseField {
export interface AsyncSelectField extends InputField {
readonly type: FieldTypes.ASYNC_SELECT
component: FieldComponents.ASYNC_SELECT
placeholder?: FormText
Expand All @@ -351,11 +352,10 @@ export interface AsyncSelectField extends BaseField {
loadingError?: FormText
backgroundColor?: InputBackgroundColor
isSearchable?: boolean
required?: boolean
isMulti?: boolean
}

export interface TextField extends BaseField {
export interface TextField extends InputField {
readonly type: FieldTypes.TEXT
component: FieldComponents.TEXT
disabled?: boolean
Expand All @@ -372,11 +372,10 @@ export interface TextField extends BaseField {
format?: string | FormatInputValueFunction
suffix?: string
rows?: number
required?: boolean
onChange?: (...event: any[]) => void
}

export interface PhoneField extends BaseField {
export interface PhoneField extends InputField {
readonly type: FieldTypes.PHONE
component: FieldComponents.PHONE
disabled?: boolean
Expand All @@ -386,7 +385,6 @@ export interface PhoneField extends BaseField {
backgroundColor?: InputBackgroundColor
allowedCountryCodes?: string[]
enableCountrySelector?: boolean
required?: boolean
onChange?: (...event: any[]) => void
}

Expand Down Expand Up @@ -559,11 +557,10 @@ export interface PdfLinkButtonField extends BaseField {
downloadButtonTitle?: StaticText
}

export interface NationalIdWithNameField extends BaseField {
export interface NationalIdWithNameField extends InputField {
readonly type: FieldTypes.NATIONAL_ID_WITH_NAME
component: FieldComponents.NATIONAL_ID_WITH_NAME
disabled?: boolean
required?: boolean
customNationalIdLabel?: StaticText
customNameLabel?: StaticText
onNationalIdChange?: (s: string) => void
Expand Down Expand Up @@ -636,11 +633,10 @@ export type TableRepeaterField = BaseField & {
format?: Record<string, (value: string) => string | StaticText>
}
}
export interface FindVehicleField extends BaseField {
export interface FindVehicleField extends InputField {
readonly type: FieldTypes.FIND_VEHICLE
component: FieldComponents.FIND_VEHICLE
disabled?: boolean
required?: boolean
additionalErrors: boolean
getDetails?: (plate: string) => Promise<unknown>
findVehicleButtonText?: FormText
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useState } from 'react'

import { formatText } from '@island.is/application/core'
import { buildFieldRequired, formatText } from '@island.is/application/core'
import { AsyncSelectField, FieldBaseProps } from '@island.is/application/types'
import { Box } from '@island.is/island-ui/core'
import {
Expand Down Expand Up @@ -65,7 +65,7 @@ export const AsyncSelectFormField: FC<React.PropsWithChildren<Props>> = ({

<Box paddingTop={2}>
<SelectController
required={required}
required={buildFieldRequired(application, required)}
dataTestId={field.dataTestId}
defaultValue={getDefaultValue(field, application)}
label={formatText(title, application, formatMessage)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import {
FieldBaseProps,
} from '@island.is/application/types'
import React, { FC } from 'react'
import { formatText, getValueViaPath } from '@island.is/application/core'
import {
buildFieldRequired,
formatText,
getValueViaPath,
} from '@island.is/application/core'

import { Box } from '@island.is/island-ui/core'
import { CompanySearchController } from '@island.is/application/ui-components'
Expand Down Expand Up @@ -45,7 +49,7 @@ export const CompanySearchFormField: FC<React.PropsWithChildren<Props>> = ({
return (
<Box marginTop={[2, 4]}>
<CompanySearchController
required={required}
required={buildFieldRequired(application, required)}
checkIfEmployerIsOnForbiddenList={checkIfEmployerIsOnForbiddenList}
shouldIncludeIsatNumber={shouldIncludeIsatNumber}
id={id}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { FC, useMemo } from 'react'

import { formatText, getValueViaPath } from '@island.is/application/core'
import {
buildFieldRequired,
formatText,
getValueViaPath,
} from '@island.is/application/core'
import {
FieldBaseProps,
DateField,
Expand Down Expand Up @@ -124,7 +128,7 @@ export const DateFormField: FC<React.PropsWithChildren<Props>> = ({
id={id}
name={id}
locale={lang}
required={required}
required={buildFieldRequired(application, required)}
excludeDates={finalExcludeDates}
minDate={finalMinDate}
maxDate={finalMaxDate}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildFieldRequired } from '@island.is/application/core'
import {
FieldBaseProps,
NationalIdWithNameField,
Expand All @@ -17,7 +18,7 @@ export const NationalIdWithNameFormField: FC<
id={field.id}
application={application}
disabled={field.disabled}
required={field.required}
required={buildFieldRequired(application, field.required)}
customNationalIdLabel={field.customNationalIdLabel}
customNameLabel={field.customNameLabel}
onNationalIdChange={field.onNationalIdChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC } from 'react'
import { useFormContext } from 'react-hook-form'
import { formatText } from '@island.is/application/core'
import { buildFieldRequired, formatText } from '@island.is/application/core'
import { FieldBaseProps, PhoneField } from '@island.is/application/types'
import { Box } from '@island.is/island-ui/core'
import {
Expand Down Expand Up @@ -75,7 +75,7 @@ export const PhoneFormField: FC<React.PropsWithChildren<Props>> = ({
}}
defaultValue={getDefaultValue(field, application)}
backgroundColor={backgroundColor}
required={required}
required={buildFieldRequired(application, required)}
/>
</Box>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
formatText,
buildFieldOptions,
getValueViaPath,
buildFieldRequired,
} from '@island.is/application/core'
import { FieldBaseProps, SelectField } from '@island.is/application/types'
import { Box } from '@island.is/island-ui/core'
Expand Down Expand Up @@ -53,7 +54,7 @@ export const SelectFormField: FC<React.PropsWithChildren<Props>> = ({

<Box paddingTop={2}>
<SelectController
required={required}
required={buildFieldRequired(application, required)}
defaultValue={
(getValueViaPath(application.answers, id) ??
getDefaultValue(field, application)) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { FC } from 'react'
import { useFormContext } from 'react-hook-form'

import { formatText } from '@island.is/application/core'
import { buildFieldRequired, formatText } from '@island.is/application/core'
import { FieldBaseProps, TextField } from '@island.is/application/types'
import { Box } from '@island.is/island-ui/core'
import {
Expand Down Expand Up @@ -90,7 +90,7 @@ export const TextFormField: FC<React.PropsWithChildren<Props>> = ({
defaultValue={getDefaultValue(field, application)}
backgroundColor={backgroundColor}
rows={rows}
required={required}
required={buildFieldRequired(application, required)}
rightAlign={rightAlign}
max={max}
min={min}
Expand Down

0 comments on commit dc89848

Please sign in to comment.