From dc89848622705122360cb0d16ee44673413d35e9 Mon Sep 17 00:00:00 2001 From: norda-gunni <161026627+norda-gunni@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:20:46 +0000 Subject: [PATCH] feat(application-system): Make required a dynamic field (#16691) * feat(application-system): Make required a dynamic field * Added tests --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../core/src/lib/fieldBuilders.spec.ts | 72 +++++++++++++++++++ .../application/core/src/lib/fieldBuilders.ts | 12 +++- libs/application/types/src/lib/Fields.ts | 36 +++++----- .../AsyncSelectFormField.tsx | 4 +- .../CompanySearchFormField.tsx | 8 ++- .../src/lib/DateFormField/DateFormField.tsx | 8 ++- .../NationalIdWithNameFormField.tsx | 3 +- .../src/lib/PhoneFormField/PhoneFormField.tsx | 4 +- .../lib/SelectFormField/SelectFormField.tsx | 3 +- .../src/lib/TextFormField/TextFormField.tsx | 4 +- 10 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 libs/application/core/src/lib/fieldBuilders.spec.ts diff --git a/libs/application/core/src/lib/fieldBuilders.spec.ts b/libs/application/core/src/lib/fieldBuilders.spec.ts new file mode 100644 index 000000000000..b3614c8c0855 --- /dev/null +++ b/libs/application/core/src/lib/fieldBuilders.spec.ts @@ -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) + }) +}) diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts index 15ed082dedce..8f510a73a6c1 100644 --- a/libs/application/core/src/lib/fieldBuilders.ts +++ b/libs/application/core/src/lib/fieldBuilders.ts @@ -44,6 +44,7 @@ import { AccordionField, BankAccountField, SliderField, + MaybeWithApplication, } from '@island.is/application/types' import { Colors } from '@island.is/island-ui/theme' @@ -532,10 +533,19 @@ export const buildFieldOptions = ( if (typeof maybeOptions === 'function') { return maybeOptions(application, field) } - return maybeOptions } +export const buildFieldRequired = ( + application: Application, + maybeRequired?: MaybeWithApplication, +) => { + if (typeof maybeRequired === 'function') { + return maybeRequired(application) + } + return maybeRequired +} + export const buildRedirectToServicePortalField = (data: { id: string title: FormText diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index a5ed41802863..688c0903a0bd 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -23,6 +23,7 @@ import { Locale } from '@island.is/shared/types' type Space = keyof typeof theme.spacing export type RecordObject = Record +export type MaybeWithApplication = T | ((application: Application) => T) export type MaybeWithApplicationAndField = | T | ((application: Application, field: Field) => T) @@ -196,10 +197,15 @@ export interface BaseField extends FormItem { isPartOfRepeater?: boolean defaultValue?: MaybeWithApplicationAndField doesNotRequireAnswer?: boolean + // TODO use something like this for non-schema validation? // validate?: (formValue: FormValue, context?: object) => boolean } +export interface InputField extends BaseField { + required?: MaybeWithApplication +} + export enum FieldTypes { CHECKBOX = 'CHECKBOX', CUSTOM = 'CUSTOM', @@ -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 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 @@ -292,7 +297,6 @@ export interface DateField extends BaseField { excludeDates?: MaybeWithApplicationAndField backgroundColor?: DatePickerBackgroundColor onChange?(date: string): void - required?: boolean readOnly?: boolean } @@ -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 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 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 @@ -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 @@ -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 @@ -386,7 +385,6 @@ export interface PhoneField extends BaseField { backgroundColor?: InputBackgroundColor allowedCountryCodes?: string[] enableCountrySelector?: boolean - required?: boolean onChange?: (...event: any[]) => void } @@ -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 @@ -636,11 +633,10 @@ export type TableRepeaterField = BaseField & { format?: Record 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 findVehicleButtonText?: FormText diff --git a/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx b/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx index 22f97bc59d4a..49388c7171b5 100644 --- a/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx +++ b/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx @@ -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 { @@ -65,7 +65,7 @@ export const AsyncSelectFormField: FC> = ({ > = ({ return ( > = ({ id={id} name={id} locale={lang} - required={required} + required={buildFieldRequired(application, required)} excludeDates={finalExcludeDates} minDate={finalMinDate} maxDate={finalMaxDate} diff --git a/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx b/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx index 88124ccc8cfa..1caf9fd5be81 100644 --- a/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx +++ b/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx @@ -1,3 +1,4 @@ +import { buildFieldRequired } from '@island.is/application/core' import { FieldBaseProps, NationalIdWithNameField, @@ -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} diff --git a/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx b/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx index 0fbcbb72c30e..067aed313ec3 100644 --- a/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx +++ b/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx @@ -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 { @@ -75,7 +75,7 @@ export const PhoneFormField: FC> = ({ }} defaultValue={getDefaultValue(field, application)} backgroundColor={backgroundColor} - required={required} + required={buildFieldRequired(application, required)} /> diff --git a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx index 1c9ae5f3ecfd..fa9bd3e182c8 100644 --- a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx +++ b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx @@ -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' @@ -53,7 +54,7 @@ export const SelectFormField: FC> = ({ > = ({ defaultValue={getDefaultValue(field, application)} backgroundColor={backgroundColor} rows={rows} - required={required} + required={buildFieldRequired(application, required)} rightAlign={rightAlign} max={max} min={min}