From a9f8193b99360c697baae8ad5ae97b004cb71fab Mon Sep 17 00:00:00 2001 From: vasileios Date: Tue, 21 Jan 2025 11:08:14 +0100 Subject: [PATCH] [open-formulieren/open-forms#5006] Updated addressNL component to manually fill in city and streetname Until now the addressNL component would retrieve the city and street name based on the postcode and housenumber. These fields were always read-only but now we want to be able to fill in city and street name if something goes wrong with the API call. --- src/formio/components/AddressNL.jsx | 114 +++++++++++++++------ src/formio/components/AddressNL.stories.js | 35 ++++++- src/i18n/compiled/en.json | 24 ++--- src/i18n/compiled/nl.json | 24 ++--- src/i18n/messages/en.json | 20 ++-- src/i18n/messages/nl.json | 20 ++-- 6 files changed, 157 insertions(+), 80 deletions(-) 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.",