diff --git a/src/formio/components/AddressNL.jsx b/src/formio/components/AddressNL.jsx index e2ab54c93..9e1331532 100644 --- a/src/formio/components/AddressNL.jsx +++ b/src/formio/components/AddressNL.jsx @@ -3,7 +3,7 @@ */ import {Formik, useFormikContext} from 'formik'; import debounce from 'lodash/debounce'; -import {useContext, useEffect} from 'react'; +import {useContext, useEffect, useState} from 'react'; import {createRoot} from 'react-dom/client'; import {Formio} from 'react-formio'; import {FormattedMessage, IntlProvider, defineMessages, useIntl} from 'react-intl'; @@ -61,6 +61,50 @@ export default class AddressNL extends Field { if (this.component.validate.plugins && this.component.validate.plugins.length) { updatedOptions.async = true; } + + if (!dirty) { + return super.checkComponentValidity(data, dirty, row, updatedOptions); + } + + const {postcode, houseNumber, city, streetName} = this.dataValue; + if (this.component?.validate?.required) { + if ( + this.component.deriveAddress && + [postcode, houseNumber, city, streetName].some(value => value === '') + ) { + const messages = [ + { + message: this.t('Required fields can not be empty.'), + level: 'error', + }, + ]; + this.setComponentValidity(messages, true, false); + return false; + } else if ( + !this.component.deriveAddress && + [postcode, houseNumber].some(value => value === '') + ) { + const messages = [ + { + message: this.t('Required fields can not be empty.'), + level: 'error', + }, + ]; + this.setComponentValidity(messages, true, false); + return false; + } + } else { + if ((postcode && !houseNumber) || (!postcode && houseNumber)) { + const messages = [ + { + message: this.t('Both postcode and housenumber fields are required or none of them.'), + level: 'error', + }, + ]; + this.setComponentValidity(messages, true, false); + return false; + } + } return super.checkComponentValidity(data, dirty, row, updatedOptions); } @@ -77,6 +121,7 @@ export default class AddressNL extends Field { city: '', streetName: '', secretStreetCity: '', + autoPopulated: false, }; } @@ -199,6 +244,22 @@ const FIELD_LABELS = defineMessages({ description: 'Label for addressNL houseNumber input', defaultMessage: 'House number', }, + houseLetter: { + description: 'Label for addressNL houseLetter input', + defaultMessage: 'House letter', + }, + houseNumberAddition: { + description: 'Label for addressNL houseNumberAddition input', + defaultMessage: 'House number addition', + }, + streetName: { + description: 'Label for addressNL streetName input', + defaultMessage: 'Street name', + }, + city: { + description: 'Label for addressNL city input', + defaultMessage: 'City', + }, }); const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => { @@ -216,6 +277,7 @@ const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => { }); let postcodeSchema = z.string().regex(postcodeRegex, {message: postcodeErrorMessage}); + let streetNameSchema = z.string(); const {pattern: cityPattern = '', errorMessage: cityErrorMessage = ''} = city; let citySchema = z.string(); if (cityPattern) { @@ -235,12 +297,15 @@ const addressNLSchema = (required, intl, {postcode = {}, city = {}}) => { if (!required) { postcodeSchema = postcodeSchema.optional(); houseNumberSchema = houseNumberSchema.optional(); + streetNameSchema = streetNameSchema.optional(); + citySchema = citySchema.optional(); } return z .object({ postcode: postcodeSchema, - city: citySchema.optional(), + streetName: streetNameSchema, + city: citySchema, houseNumber: houseNumberSchema, houseLetter: z .string() @@ -340,6 +405,7 @@ const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormi postcode: true, houseNumber: true, city: true, + streetName: true, }} validationSchema={toFormikValidationSchema( addressNLSchema(required, intl, { @@ -368,6 +434,7 @@ const AddressNLForm = ({initialValues, required, deriveAddress, layout, setFormi const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => { const {values, isValid, setFieldValue} = useFormikContext(); const {baseUrl} = useContext(ConfigContext); + const [isAddressAutoFilled, setAddressAutoFilled] = useState(true); const useColumns = layout === 'doubleColumn'; useEffect(() => { @@ -394,6 +461,12 @@ const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => { setFieldValue('city', data['city']); setFieldValue('streetName', data['streetName']); setFieldValue('secretStreetCity', data['secretStreetCity']); + + // mark the auto-filled fields as populated and disabled when they have been both + // retrieved from the API and they do have a value + const dataRetrieved = !!(data['city'] && data['streetName']); + setAddressAutoFilled(dataRetrieved); + setFieldValue('autoPopulated', dataRetrieved); }; return ( @@ -406,45 +479,24 @@ const FormikAddress = ({required, setFormioValues, deriveAddress, layout}) => { > - - } - /> + } /> - } + label={} /> {deriveAddress && ( <> - } - disabled + label={} + disabled={isAddressAutoFilled} + isRequired={required} /> - } - disabled + label={} + disabled={isAddressAutoFilled} + isRequired={required} /> )} diff --git a/src/formio/components/AddressNL.stories.js b/src/formio/components/AddressNL.stories.js index 20bd619c8..8db81161b 100644 --- a/src/formio/components/AddressNL.stories.js +++ b/src/formio/components/AddressNL.stories.js @@ -1,6 +1,7 @@ import {expect, userEvent, waitFor, within} from '@storybook/test'; import {ConfigDecorator, withUtrechtDocument} from 'story-utils/decorators'; +import {sleep} from 'utils'; import { mockBAGDataGet, @@ -84,6 +85,28 @@ export const ClientSideValidation = { }, }; +export const Required = { + args: { + extraComponentProperties: { + validate: { + required: true, + }, + }, + }, + render: SingleFormioComponent, + play: async ({canvasElement}) => { + await sleep(500); + const canvas = within(canvasElement); + + (await canvas.findByLabelText('Huisnummer')).focus(); + await userEvent.tab(); + + expect(await canvas.findByText('Postcode is verplicht.')).toBeVisible(); + expect(await canvas.findByText('Huisnummer is verplicht.')).toBeVisible(); + expect(await canvas.findByText('required')).toBeVisible(); + }, +}; + export const NotRequired = { args: { extraComponentProperties: { @@ -195,7 +218,7 @@ export const WithFailedBRKValidation = { // }, }; -export const WithDeriveCityStreetNameWithData = { +export const WithDeriveCityStreetNameWithDataNotRequired = { render: SingleFormioComponent, parameters: { msw: { @@ -234,7 +257,7 @@ export const WithDeriveCityStreetNameWithData = { const houseNumberInput = await canvas.findByLabelText('Huisnummer'); await userEvent.type(houseNumberInput, '1'); - const city = await canvas.findByLabelText('Stad'); + const city = await canvas.findByLabelText('Plaats'); const streetName = await canvas.findByLabelText('Straatnaam'); await userEvent.tab(); @@ -322,7 +345,7 @@ export const WithDeriveCityStreetNameWithDataIncorrectCity = { const houseNumberInput = await canvas.findByLabelText('Huisnummer'); await userEvent.type(houseNumberInput, '1'); - const city = await canvas.findByLabelText('Stad'); + const city = await canvas.findByLabelText('Plaats'); const streetName = await canvas.findByLabelText('Straatnaam'); await userEvent.tab(); @@ -338,7 +361,7 @@ export const WithDeriveCityStreetNameWithDataIncorrectCity = { }, }; -export const WithDeriveCityStreetNameNoData = { +export const WithDeriveCityStreetNameNoDataAndNotRequired = { render: SingleFormioComponent, parameters: { msw: { @@ -365,14 +388,16 @@ export const WithDeriveCityStreetNameNoData = { const houseNumberInput = await canvas.findByLabelText('Huisnummer'); await userEvent.type(houseNumberInput, '1'); - const city = await canvas.findByLabelText('Stad'); + const city = await canvas.findByLabelText('Plaats'); const streetName = await canvas.findByLabelText('Straatnaam'); await userEvent.tab(); await waitFor(() => { expect(city.value).toBe(''); + expect(city).not.toBeDisabled(); expect(streetName.value).toBe(''); + expect(streetName).not.toBeDisabled(); }); }, }; diff --git a/src/i18n/compiled/en.json b/src/i18n/compiled/en.json index 69d4d9e5d..cd7ed52c5 100644 --- a/src/i18n/compiled/en.json +++ b/src/i18n/compiled/en.json @@ -397,6 +397,12 @@ "value": "Check and confirm" } ], + "AKhmW+": [ + { + "type": 0, + "value": "Street name" + } + ], "AM6xqd": [ { "type": 0, @@ -491,12 +497,6 @@ "value": "Remove" } ], - "DEetjI": [ - { - "type": 0, - "value": "Street name" - } - ], "DK2ewv": [ { "type": 0, @@ -1925,12 +1925,6 @@ "value": "." } ], - "osSl3z": [ - { - "type": 0, - "value": "City" - } - ], "ovI+W7": [ { "type": 0, @@ -2069,6 +2063,12 @@ "value": "Send code" } ], + "s4+4p2": [ + { + "type": 0, + "value": "City" + } + ], "sSmY1N": [ { "type": 0, diff --git a/src/i18n/compiled/nl.json b/src/i18n/compiled/nl.json index a57a85100..4ab79b983 100644 --- a/src/i18n/compiled/nl.json +++ b/src/i18n/compiled/nl.json @@ -397,6 +397,12 @@ "value": "Controleer en bevestig" } ], + "AKhmW+": [ + { + "type": 0, + "value": "Straatnaam" + } + ], "AM6xqd": [ { "type": 0, @@ -491,12 +497,6 @@ "value": "Verwijderen" } ], - "DEetjI": [ - { - "type": 0, - "value": "Straatnaam" - } - ], "DK2ewv": [ { "type": 0, @@ -1929,12 +1929,6 @@ "value": " zijn." } ], - "osSl3z": [ - { - "type": 0, - "value": "Stad" - } - ], "ovI+W7": [ { "type": 0, @@ -2073,6 +2067,12 @@ "value": "Verstuur code" } ], + "s4+4p2": [ + { + "type": 0, + "value": "Plaats" + } + ], "sSmY1N": [ { "type": 0, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 965f9c360..2e5b3ab7b 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -174,6 +174,11 @@ "description": "Check overview and confirm", "originalDefault": "Check and confirm" }, + "AKhmW+": { + "defaultMessage": "Street name", + "description": "Label for addressNL streetName input", + "originalDefault": "Street name" + }, "AM6xqd": { "defaultMessage": "House number must be a number with up to five digits (e.g. 456).", "description": "ZOD error message when AddressNL house number does not match the house number regular expression", @@ -249,11 +254,6 @@ "description": "Appointments: remove product/service button text", "originalDefault": "Remove" }, - "DEetjI": { - "defaultMessage": "Street name", - "description": "Label for addressNL streetName read only result", - "originalDefault": "Street name" - }, "DK2ewv": { "defaultMessage": "Authentication problem", "description": "'Permission denied' error title", @@ -934,11 +934,6 @@ "description": "ZOD 'too_big' error message, for BigInt", "originalDefault": "BigInt must be {exact, select, true {exactly equal to} other {{inclusive, select, true {less than or equal to} other {less than}}} } {maximum}." }, - "osSl3z": { - "defaultMessage": "City", - "description": "Label for addressNL city read only result", - "originalDefault": "City" - }, "ovI+W7": { "defaultMessage": "Use ⌘ + scroll to zoom the map", "description": "Gesturehandeling mac scroll message.", @@ -984,6 +979,11 @@ "description": "Email verification: send code button text", "originalDefault": "Send code" }, + "s4+4p2": { + "defaultMessage": "City", + "description": "Label for addressNL city input", + "originalDefault": "City" + }, "sSmY1N": { "defaultMessage": "Find address", "description": "The leaflet map's input fields placeholder message.", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 4ef35f536..73fed4ab6 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -176,6 +176,11 @@ "description": "Check overview and confirm", "originalDefault": "Check and confirm" }, + "AKhmW+": { + "defaultMessage": "Straatnaam", + "description": "Label for addressNL streetName input", + "originalDefault": "Street name" + }, "AM6xqd": { "defaultMessage": "Huisnummer moet een nummer zijn met maximaal 5 cijfers (bijv. 456).", "description": "ZOD error message when AddressNL house number does not match the house number regular expression", @@ -252,11 +257,6 @@ "description": "Appointments: remove product/service button text", "originalDefault": "Remove" }, - "DEetjI": { - "defaultMessage": "Straatnaam", - "description": "Label for addressNL streetName read only result", - "originalDefault": "Street name" - }, "DK2ewv": { "defaultMessage": "Inlogprobleem", "description": "'Permission denied' error title", @@ -946,11 +946,6 @@ "description": "ZOD 'too_big' error message, for BigInt", "originalDefault": "BigInt must be {exact, select, true {exactly equal to} other {{inclusive, select, true {less than or equal to} other {less than}}} } {maximum}." }, - "osSl3z": { - "defaultMessage": "Stad", - "description": "Label for addressNL city read only result", - "originalDefault": "City" - }, "ovI+W7": { "defaultMessage": "Gebruik ⌘ + scroll om te zoomen in de kaart", "description": "Gesturehandeling mac scroll message.", @@ -996,6 +991,11 @@ "description": "Email verification: send code button text", "originalDefault": "Send code" }, + "s4+4p2": { + "defaultMessage": "Plaats", + "description": "Label for addressNL city input", + "originalDefault": "City" + }, "sSmY1N": { "defaultMessage": "Zoek adres", "description": "The leaflet map's input fields placeholder message.",