Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(application-system): Make required a dynamic field #16691

Merged
merged 3 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
norda-gunni marked this conversation as resolved.
Show resolved Hide resolved

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)}
norda-gunni marked this conversation as resolved.
Show resolved Hide resolved
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