From bc20cda312636dd6ee880708a5608d6027ff7692 Mon Sep 17 00:00:00 2001 From: Mike van den Hoek Date: Thu, 5 Nov 2026 12:28:39 +0100 Subject: [PATCH] feat: migrate forms and personal data blocks from V1 to V2 supplier --- CHANGELOG.md | 6 + README.md | 16 + .../personal-data-row/index.asset.php | 2 +- .../personal-data-row/index.js | 2 +- build/supplier-migration.asset.php | 1 + build/supplier-migration.js | 1 + config/supplier-migrations/block_options.php | 19 ++ .../supplier-migrations/sources/enable-u.php | 16 + .../sources/pink-roccade.php | 17 + .../supplier-migrations/sources/vrij-brp.php | 46 +++ config/supplier-migrations/targets.php | 16 + resources/js/admin/supplier-migration.js | 34 ++ .../config/personalDataOptions.js | 24 +- .../config/supplierOptions.js | 10 + .../personal-data-row/edit.js | 27 +- .../Console/MigrateSupplierCommand.php | 153 +++++++++ .../GravityForms/GravityFormsAddon.php | 78 +++++ .../SupplierMigrationSettings.php | 215 ++++++++++++ .../Providers/GravityFormsServiceProvider.php | 15 + .../Services/SupplierMigrationService.php | 313 ++++++++++++++++++ .../SupplierMigration/V1ToV2FieldMapping.php | 103 ++++++ webpack.config.js | 1 + 22 files changed, 1108 insertions(+), 7 deletions(-) create mode 100644 build/supplier-migration.asset.php create mode 100644 build/supplier-migration.js create mode 100644 config/supplier-migrations/block_options.php create mode 100644 config/supplier-migrations/sources/enable-u.php create mode 100644 config/supplier-migrations/sources/pink-roccade.php create mode 100644 config/supplier-migrations/sources/vrij-brp.php create mode 100644 config/supplier-migrations/targets.php create mode 100644 resources/js/admin/supplier-migration.js create mode 100644 src/PrefillGravityForms/Console/MigrateSupplierCommand.php create mode 100644 src/PrefillGravityForms/GravityForms/SupplierMigrationSettings.php create mode 100644 src/PrefillGravityForms/Services/SupplierMigrationService.php create mode 100644 src/PrefillGravityForms/SupplierMigration/V1ToV2FieldMapping.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ae638b4..6b8bccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +- Added: migrate forms and personal data blocks from a V1 supplier (EnableU, PinkRoccade, VrijBRP) to a V2 supplier (EnableU V2, PinkRoccade V2) when changing the supplier in the add-on settings, after confirmation +- Changed: personal data block only shows the options of the API version of the selected supplier +- Added: WP-CLI command `wp prefill migrate-supplier` to migrate forms and personal data blocks to a V2 supplier + ## [v2.1.1] - 2026-10-27 - Added: make log directory configurable to keep log files outside the webroot diff --git a/README.md b/README.md index c414c9f..1472217 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,22 @@ In addition, at least one of the following plug-ins must be installed to enable 2. Go to the form settings of the form you want to configure. 3. Scroll down and look for the 'OWC Prefill' panel and configure the settings. +### Migrating from a V1 to a V2 supplier + +When the supplier in the add-on settings is changed from a V1 supplier (EnableU, PinkRoccade or VrijBRP) to a V2 supplier (EnableU V2 or PinkRoccade V2), you will be asked to migrate the linked forms and personal data blocks. Every V1 supplier can be migrated to every V2 supplier, e.g. VrijBRP to EnableU V2. +After confirming, the supplier in the form settings is switched and the linked fields are mapped to their V2 counterparts, see `config/supplier-migrations/`. +Fields without a V2 counterpart and forms using a mapping file from the theme are reported after saving and need to be updated manually. +The original values are kept in the form setting `owc-form-setting-supplier-legacy` and the field property `linkedFieldValueLegacy`. + +The migration can also be run with WP-CLI, use `--dry-run` to see what will be migrated first: + +```sh +wp prefill migrate-supplier enable-u enable-u-v2 --dry-run +wp prefill migrate-supplier vrij-brp enable-u-v2 +``` + +On a multisite, add `--url=` to migrate a specific site. + ### Cache Encryption To enable secure caching of sensitive data, you **must define an encryption key** in your `wp-config.php` file. This key is used to encrypt and decrypt the cached data and should be kept secret at all times. diff --git a/build/personal-data-table/personal-data-row/index.asset.php b/build/personal-data-table/personal-data-row/index.asset.php index b231319..293d873 100644 --- a/build/personal-data-table/personal-data-row/index.asset.php +++ b/build/personal-data-table/personal-data-row/index.asset.php @@ -1 +1 @@ - ['react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'], 'version' => 'df0a722bfd18e3eb3f64']; + ['react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'], 'version' => '634ba1e632324dd12941']; diff --git a/build/personal-data-table/personal-data-row/index.js b/build/personal-data-table/personal-data-row/index.js index 78a1ced..7f915c3 100644 --- a/build/personal-data-table/personal-data-row/index.js +++ b/build/personal-data-table/personal-data-row/index.js @@ -1 +1 @@ -(()=>{"use strict";const e=window.wp.blocks,a=(window.wp.i18n,window.wp.blockEditor),l=window.wp.components,n=window.wp.data,t=window.wp.element,i=[{value:"",label:"Selecteer persoonsgegevens",disabled:!0},{label:"Burgerservicenummer (BSN)",value:"burgerservicenummer"},{label:"Geslacht",value:"geslachtsaanduiding"},{label:"Leeftijd",value:"leeftijd"},{label:"Achternaam",value:"naam.geslachtsnaam"},{label:"Voorvoegsel (tussenvoegsel)",value:"naam.voorvoegsel"},{label:"Voorletters",value:"naam.voorletters"},{label:"Voornaam",value:"naam.voornaam"},{label:"Voornamen",value:"naam.voornamen"},{label:"Aanduiding Naamgebruik (V1)",value:"naam.aanduidingNaamgebruik"},{label:"Aanduiding Naamgebruik (V2)",value:"naam.aanduidingNaamgebruik.omschrijving"},{label:"Aanschrijfwijze",value:"naam.aanschrijfwijze"},{label:"Gebruik in lopende tekst",value:"naam.gebruikInLopendeTekst"},{label:"Geboortedatum",value:"geboorte.datum.datum"},{label:"Straat",value:"verblijfplaats.straat"},{label:"Straatnaam",value:"verblijfplaats.straatnaam"},{label:"Huisnummer",value:"verblijfplaats.huisnummer"},{label:"Huisnummertoevoeging",value:"verblijfplaats.huisletter"},{label:"Postcode",value:"verblijfplaats.postcode"},{label:"Woonplaats",value:"verblijfplaats.woonplaats"},{label:"Woonplaats naam",value:"verblijfplaats.woonplaatsnaam"}],o=[{value:"",label:"Selecteer een leverancier",disabled:!0},{label:"EnableU",value:"EnableU"},{label:"EnableU V2",value:"EnableUV2"},{label:"OpenZaak",value:"OpenZaak"},{label:"PinkRoccade",value:"PinkRoccade"},{label:"PinkRoccadeV2",value:"PinkRoccadeV2"},{label:"VrijBRP",value:"VrijBRP"},{label:"WeAreFrank",value:"WeAreFrank"}],s=window.ReactJSXRuntime,r=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":3,"name":"owc-prefill-gravity-forms/personal-data-row","version":"0.1.0","title":"Persoonsgegevens","icon":"id","description":"Toon persoonsgegevens van de ingelogde gebruiker.","example":{},"supports":{"html":false},"attributes":{"selectedSupplier":{"type":"object","default":{"value":"","label":""}},"goalBinding":{"type":"string","default":""},"processing":{"type":"string","default":""},"selectedOption":{"type":"object","default":{"value":"","label":""}},"isChildOfTable":{"type":"boolean","default":false},"htmlElement":{"type":"string","default":"p"}},"textdomain":"prefill-gravity-forms","editorScript":"file:./index.js","render":"file:./render.php"}');(0,e.registerBlockType)(r.name,{...r,edit:function({attributes:e,setAttributes:r,clientId:d}){const{selectedSupplier:v,goalBinding:c,processing:u,selectedOption:b,htmlElement:p,isChildOfTable:g}=e,{blockParents:m}=(0,n.useSelect)(e=>({blockParents:e("core/block-editor").getBlockNamesByClientId(e("core/block-editor").getBlockParents(d))}));(0,t.useEffect)(()=>{m.length>0&&"owc-prefill-gravity-forms/personal-data-table"===m[m.length-1]&&r({isChildOfTable:!0})},[m,r]);const h=(0,s.jsx)(l.SelectControl,{help:"Selecteer de juiste leverancier die ingesteld staat in de algemene instellingen.",label:"Leverancier",value:v.value,options:o,onChange:e=>{const a={value:e,label:o.find(a=>a.value===e).label};r({selectedSupplier:a})}}),j=(0,s.jsx)(l.TextControl,{help:"Algemene doelbinding voor verzoeken zonder specifieke doelbinding. Beschrijft waarom de persoonsgegevens worden opgevraagd en op basis van welke wettelijke taak of bevoegdheid de organisatie dit doet. De mogelijke waarden zijn implementatieafhankelijk en worden door de leverancier bepaald.",label:"Doelbinding (v2)",value:c,onChange:e=>{r({goalBinding:e})}}),k=(0,s.jsx)(l.TextControl,{help:"Beschrijft voor welk werkproces of welke processtap de BRP-gegevens worden opgevraagd. De mogelijke waarden zijn implementatieafhankelijk en dienen door de leverancier te worden aangeleverd.",label:"Verwerking (v2)",value:u,onChange:e=>{r({processing:e})}}),w=(0,s.jsx)(l.SelectControl,{label:"Automatisch invullen",value:b.value,options:i,onChange:e=>{const a={value:e,label:i.find(a=>a.value===e).label};r({selectedOption:a})}}),f=(0,s.jsx)(l.SelectControl,{label:"HTML Element",value:p,options:[{label:"
",value:"div"},{label:"

",value:"p"},{label:"",value:"span"}],onChange:e=>{r({htmlElement:e})}});return(0,s.jsxs)("div",{...(0,a.useBlockProps)(),children:[(0,s.jsx)(a.InspectorControls,{children:(0,s.jsx)(l.TabPanel,{activeClass:"active-tab",tabs:[{name:"settings",title:"Settings"}],children:e=>(0,s.jsx)(l.PanelBody,{initialOpen:!0,children:"settings"===e.name&&(0,s.jsxs)(s.Fragment,{children:[h,j,k,w,!g&&f]})})})}),void 0===b.value||""===b.value?(0,s.jsx)("p",{children:w}):(0,s.jsx)(s.Fragment,{children:(0,s.jsxs)("div",{className:"row",children:[g&&(0,s.jsx)("div",{className:"col-6 font-weight-bold",children:(0,s.jsx)("p",{children:b.label})}),(0,s.jsxs)("div",{className:"col-6",children:["[",(x=b.label,x?x.charAt(0).toLowerCase()+x.slice(1):x),"]"]})]})})]});var x},save:()=>null})})(); \ No newline at end of file +(()=>{"use strict";const e=window.wp.blocks,a=(window.wp.i18n,window.wp.blockEditor),l=window.wp.components,n=window.wp.data,i=window.wp.element,o=[{value:"",label:"Selecteer persoonsgegevens",disabled:!0},{label:"Burgerservicenummer (BSN)",value:"burgerservicenummer"},{label:"Geslacht",value:"geslachtsaanduiding"},{label:"Leeftijd",value:"leeftijd"},{label:"Achternaam",value:"naam.geslachtsnaam"},{label:"Voorvoegsel (tussenvoegsel)",value:"naam.voorvoegsel"},{label:"Voorletters",value:"naam.voorletters"},{label:"Voornaam",value:"naam.voornaam"},{label:"Voornamen",value:"naam.voornamen"},{label:"Aanduiding Naamgebruik",value:"naam.aanduidingNaamgebruik",version:"1"},{label:"Aanduiding Naamgebruik",value:"naam.aanduidingNaamgebruik.omschrijving",version:"2"},{label:"Aanschrijfwijze",value:"naam.aanschrijfwijze"},{label:"Gebruik in lopende tekst",value:"naam.gebruikInLopendeTekst"},{label:"Geboortedatum",value:"geboorte.datum.datum"},{label:"Straat",value:"verblijfplaats.straat",version:"1"},{label:"Straatnaam",value:"verblijfplaats.straatnaam",version:"1"},{label:"Straat",value:"verblijfplaats.verblijfadres.officieleStraatnaam",version:"2"},{label:"Huisnummer",value:"verblijfplaats.huisnummer"},{label:"Huisnummertoevoeging",value:"verblijfplaats.huisletter"},{label:"Postcode",value:"verblijfplaats.postcode"},{label:"Woonplaats",value:"verblijfplaats.woonplaats",version:"1"},{label:"Woonplaats naam",value:"verblijfplaats.woonplaatsnaam",version:"1"},{label:"Woonplaats",value:"verblijfplaats.verblijfadres.woonplaats",version:"2"}],t=[{value:"",label:"Selecteer een leverancier",disabled:!0},{label:"EnableU",value:"EnableU",version:"1"},{label:"EnableU V2",value:"EnableUV2",version:"2"},{label:"OpenZaak",value:"OpenZaak",version:"1"},{label:"PinkRoccade",value:"PinkRoccade",version:"1"},{label:"PinkRoccadeV2",value:"PinkRoccadeV2",version:"2"},{label:"VrijBRP",value:"VrijBRP",version:"1"},{label:"WeAreFrank",value:"WeAreFrank",version:"2"}],r=window.ReactJSXRuntime,s=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":3,"name":"owc-prefill-gravity-forms/personal-data-row","version":"0.1.0","title":"Persoonsgegevens","icon":"id","description":"Toon persoonsgegevens van de ingelogde gebruiker.","example":{},"supports":{"html":false},"attributes":{"selectedSupplier":{"type":"object","default":{"value":"","label":""}},"goalBinding":{"type":"string","default":""},"processing":{"type":"string","default":""},"selectedOption":{"type":"object","default":{"value":"","label":""}},"isChildOfTable":{"type":"boolean","default":false},"htmlElement":{"type":"string","default":"p"}},"textdomain":"prefill-gravity-forms","editorScript":"file:./index.js","render":"file:./render.php"}');(0,e.registerBlockType)(s.name,{...s,edit:function({attributes:e,setAttributes:s,clientId:v}){var d;const{selectedSupplier:u,goalBinding:b,processing:c,selectedOption:p,htmlElement:g,isChildOfTable:m}=e,{blockParents:f}=(0,n.useSelect)(e=>({blockParents:e("core/block-editor").getBlockNamesByClientId(e("core/block-editor").getBlockParents(v))}));(0,i.useEffect)(()=>{f.length>0&&"owc-prefill-gravity-forms/personal-data-table"===f[f.length-1]&&s({isChildOfTable:!0})},[f,s]);const j=t.find(e=>e.value===u.value)?.version,h=o.filter(e=>!e.version||!j||e.version===j||e.value===p.value).map(({label:e,value:a,disabled:l})=>({label:e,value:a,disabled:l})),w=(null!==(d=p.label)&&void 0!==d?d:"").replace(/\s*\(V\d+\)$/,""),k=(0,r.jsx)(l.SelectControl,{help:"Selecteer de juiste leverancier die ingesteld staat in de algemene instellingen.",label:"Leverancier",value:u.value,options:t,onChange:e=>{const a={value:e,label:t.find(a=>a.value===e).label};s({selectedSupplier:a})}}),x=(0,r.jsx)(l.TextControl,{help:"Algemene doelbinding voor verzoeken zonder specifieke doelbinding. Beschrijft waarom de persoonsgegevens worden opgevraagd en op basis van welke wettelijke taak of bevoegdheid de organisatie dit doet. De mogelijke waarden zijn implementatieafhankelijk en worden door de leverancier bepaald.",label:"Doelbinding (v2)",value:b,onChange:e=>{s({goalBinding:e})}}),S=(0,r.jsx)(l.TextControl,{help:"Beschrijft voor welk werkproces of welke processtap de BRP-gegevens worden opgevraagd. De mogelijke waarden zijn implementatieafhankelijk en dienen door de leverancier te worden aangeleverd.",label:"Verwerking (v2)",value:c,onChange:e=>{s({processing:e})}}),C=(0,r.jsx)(l.SelectControl,{label:"Automatisch invullen",value:p.value,options:h,onChange:e=>{const a={value:e,label:o.find(a=>a.value===e).label};s({selectedOption:a})}}),B=(0,r.jsx)(l.SelectControl,{label:"HTML Element",value:g,options:[{label:"

",value:"div"},{label:"

",value:"p"},{label:"",value:"span"}],onChange:e=>{s({htmlElement:e})}});return(0,r.jsxs)("div",{...(0,a.useBlockProps)(),children:[(0,r.jsx)(a.InspectorControls,{children:(0,r.jsx)(l.TabPanel,{activeClass:"active-tab",tabs:[{name:"settings",title:"Settings"}],children:e=>(0,r.jsx)(l.PanelBody,{initialOpen:!0,children:"settings"===e.name&&(0,r.jsxs)(r.Fragment,{children:[k,x,S,C,!m&&B]})})})}),void 0===p.value||""===p.value?(0,r.jsx)("p",{children:C}):(0,r.jsx)(r.Fragment,{children:(0,r.jsxs)("div",{className:"row",children:[m&&(0,r.jsx)("div",{className:"col-6 font-weight-bold",children:(0,r.jsx)("p",{children:w})}),(0,r.jsxs)("div",{className:"col-6",children:["[",(P=w,P?P.charAt(0).toLowerCase()+P.slice(1):P),"]"]})]})})]});var P},save:()=>null})})(); \ No newline at end of file diff --git a/build/supplier-migration.asset.php b/build/supplier-migration.asset.php new file mode 100644 index 0000000..4e2a1fc --- /dev/null +++ b/build/supplier-migration.asset.php @@ -0,0 +1 @@ + [], 'version' => '7f54652d5ce551171503']; diff --git a/build/supplier-migration.js b/build/supplier-migration.js new file mode 100644 index 0000000..8883a54 --- /dev/null +++ b/build/supplier-migration.js @@ -0,0 +1 @@ +!function(e){if(!e)return;const n=document.getElementById("gform-settings");if(!n)return;const t=n.querySelector(`[name="${e.supplierInput}"]`),r=n.querySelector(`[name="${e.confirmedInput}"]`);t&&r&&n.addEventListener("submit",()=>{const n=e.targets.includes(t.value);r.value=n&&window.confirm(e.message.replace("%s",t.value))?"1":""})}(window.owcPrefillSupplierMigration); \ No newline at end of file diff --git a/config/supplier-migrations/block_options.php b/config/supplier-migrations/block_options.php new file mode 100644 index 0000000..2970cf5 --- /dev/null +++ b/config/supplier-migrations/block_options.php @@ -0,0 +1,19 @@ + ['value' => 'verblijfplaats.verblijfadres.officieleStraatnaam', 'label' => 'Straat'], + 'verblijfplaats.straatnaam' => ['value' => 'verblijfplaats.verblijfadres.officieleStraatnaam', 'label' => 'Straat'], + 'verblijfplaats.woonplaats' => ['value' => 'verblijfplaats.verblijfadres.woonplaats', 'label' => 'Woonplaats'], + 'verblijfplaats.woonplaatsnaam' => ['value' => 'verblijfplaats.verblijfadres.woonplaats', 'label' => 'Woonplaats'], + 'naam.aanduidingNaamgebruik' => ['value' => 'naam.aanduidingNaamgebruik.omschrijving', 'label' => 'Aanduiding Naamgebruik'], +]; diff --git a/config/supplier-migrations/sources/enable-u.php b/config/supplier-migrations/sources/enable-u.php new file mode 100644 index 0000000..101e632 --- /dev/null +++ b/config/supplier-migrations/sources/enable-u.php @@ -0,0 +1,16 @@ + 'verblijfplaats.verblijfadres.officieleStraatnaam', + 'verblijfplaats.woonplaatsnaam' => 'verblijfplaats.verblijfadres.woonplaats', +]); diff --git a/config/supplier-migrations/sources/pink-roccade.php b/config/supplier-migrations/sources/pink-roccade.php new file mode 100644 index 0000000..10712f4 --- /dev/null +++ b/config/supplier-migrations/sources/pink-roccade.php @@ -0,0 +1,17 @@ + 'verblijfplaats.verblijfadres.officieleStraatnaam', + 'verblijfplaats.woonplaats' => 'verblijfplaats.verblijfadres.woonplaats', + 'verblijfplaats.huisnummertoevoeging' => null, +]); diff --git a/config/supplier-migrations/sources/vrij-brp.php b/config/supplier-migrations/sources/vrij-brp.php new file mode 100644 index 0000000..ef60b97 --- /dev/null +++ b/config/supplier-migrations/sources/vrij-brp.php @@ -0,0 +1,46 @@ + null, + 'soortVerbintenis' => 'soortVerbintenis.omschrijving', + 'aangaanHuwelijkPartnerschap.datum.datum' => 'aangaanHuwelijkPartnerschap.datum.datum', + 'aangaanHuwelijkPartnerschap.datum.dag' => null, + 'aangaanHuwelijkPartnerschap.datum.maand' => null, + 'aangaanHuwelijkPartnerschap.datum.jaar' => null, + 'aangaanHuwelijkPartnerschap.land.code' => null, + 'aangaanHuwelijkPartnerschap.land.omschrijving' => 'aangaanHuwelijkPartnerschap.land.omschrijving', + 'aangaanHuwelijkPartnerschap.plaats.code' => null, + 'aangaanHuwelijkPartnerschap.plaats.omschrijving' => 'aangaanHuwelijkPartnerschap.plaats.omschrijving', +] + V1ToV2FieldMapping::relative(); + +return array_merge( + V1ToV2FieldMapping::base(), + [ + 'naam.gebruikInLopendeTekst' => null, + 'naam.aanduidingNaamgebruik.code' => 'naam.aanduidingNaamgebruik.code', + 'naam.aanduidingNaamgebruik.omschrijving' => 'naam.aanduidingNaamgebruik.omschrijving', + 'nationaliteiten.0.nationaliteit.omschrijving' => 'nationaliteiten.0.nationaliteit.omschrijving', + 'nationaliteiten.0.nationaliteit.code' => 'nationaliteiten.0.nationaliteit.code', + 'geboorte.datum.dag' => null, + 'geboorte.datum.maand' => null, + 'geboorte.plaats.code' => null, + 'geboorte.land.code' => null, + 'verblijfplaats.straat' => 'verblijfplaats.verblijfadres.officieleStraatnaam', + 'verblijfplaats.straatnaam' => 'verblijfplaats.verblijfadres.officieleStraatnaam', + 'verblijfplaats.woonplaats' => 'verblijfplaats.verblijfadres.woonplaats', + 'verblijfplaats.woonplaatsnaam' => 'verblijfplaats.verblijfadres.woonplaats', + ], + V1ToV2FieldMapping::prefix($partner, '_embedded.partners.0', 'partners.0') +); diff --git a/config/supplier-migrations/targets.php b/config/supplier-migrations/targets.php new file mode 100644 index 0000000..c0fdac4 --- /dev/null +++ b/config/supplier-migrations/targets.php @@ -0,0 +1,16 @@ + ['block_supplier_label' => 'EnableU V2'], + 'pink-roccade-v2' => ['block_supplier_label' => 'PinkRoccadeV2'], +]; diff --git a/resources/js/admin/supplier-migration.js b/resources/js/admin/supplier-migration.js new file mode 100644 index 0000000..5cd750b --- /dev/null +++ b/resources/js/admin/supplier-migration.js @@ -0,0 +1,34 @@ +/** + * Asks for confirmation on the add-on settings page when the supplier is changed from a V1 supplier to a V2 + * supplier, e.g. from VrijBRP to EnableU V2. The answer is stored in a hidden input and handled when saving. + * + * The config is provided by SupplierMigrationSettings::enqueueConfirmScript() as window.owcPrefillSupplierMigration. + */ +( function ( config ) { + if ( ! config ) { + return; + } + + const form = document.getElementById( 'gform-settings' ); + + if ( ! form ) { + return; + } + + const supplier = form.querySelector( `[name="${ config.supplierInput }"]` ); + const confirmed = form.querySelector( `[name="${ config.confirmedInput }"]` ); + + if ( ! supplier || ! confirmed ) { + return; + } + + form.addEventListener( 'submit', () => { + const isTarget = config.targets.includes( supplier.value ); + + confirmed.value = + isTarget && + window.confirm( config.message.replace( '%s', supplier.value ) ) + ? '1' + : ''; + } ); +} )( window.owcPrefillSupplierMigration ); diff --git a/resources/js/blocks/personal-data-table/personal-data-row/config/personalDataOptions.js b/resources/js/blocks/personal-data-table/personal-data-row/config/personalDataOptions.js index 0ac5977..fd8ab28 100644 --- a/resources/js/blocks/personal-data-table/personal-data-row/config/personalDataOptions.js +++ b/resources/js/blocks/personal-data-table/personal-data-row/config/personalDataOptions.js @@ -1,3 +1,7 @@ +/** + * Options without a version are available for every supplier, these are translated on runtime for V2 suppliers + * by the PersonalDataService. Options with a version are only available for suppliers of that API version. + */ const personalDataOptions = [ { value: '', @@ -37,12 +41,14 @@ const personalDataOptions = [ value: 'naam.voornamen', }, { - label: 'Aanduiding Naamgebruik (V1)', + label: 'Aanduiding Naamgebruik', value: 'naam.aanduidingNaamgebruik', + version: '1', }, { - label: 'Aanduiding Naamgebruik (V2)', + label: 'Aanduiding Naamgebruik', value: 'naam.aanduidingNaamgebruik.omschrijving', + version: '2', }, { label: 'Aanschrijfwijze', @@ -59,10 +65,17 @@ const personalDataOptions = [ { label: 'Straat', value: 'verblijfplaats.straat', + version: '1', }, { label: 'Straatnaam', value: 'verblijfplaats.straatnaam', + version: '1', + }, + { + label: 'Straat', + value: 'verblijfplaats.verblijfadres.officieleStraatnaam', + version: '2', }, { label: 'Huisnummer', @@ -79,10 +92,17 @@ const personalDataOptions = [ { label: 'Woonplaats', value: 'verblijfplaats.woonplaats', + version: '1', }, { label: 'Woonplaats naam', value: 'verblijfplaats.woonplaatsnaam', + version: '1', + }, + { + label: 'Woonplaats', + value: 'verblijfplaats.verblijfadres.woonplaats', + version: '2', }, ]; diff --git a/resources/js/blocks/personal-data-table/personal-data-row/config/supplierOptions.js b/resources/js/blocks/personal-data-table/personal-data-row/config/supplierOptions.js index 0a78334..560fa2e 100644 --- a/resources/js/blocks/personal-data-table/personal-data-row/config/supplierOptions.js +++ b/resources/js/blocks/personal-data-table/personal-data-row/config/supplierOptions.js @@ -1,3 +1,6 @@ +/** + * The version is the HaalCentraal BRP API version of the supplier, used to filter the personal data options. + */ const supplierOptions = [ { value: '', @@ -7,30 +10,37 @@ const supplierOptions = [ { label: 'EnableU', value: 'EnableU', + version: '1', }, { label: 'EnableU V2', value: 'EnableUV2', + version: '2', }, { label: 'OpenZaak', value: 'OpenZaak', + version: '1', }, { label: 'PinkRoccade', value: 'PinkRoccade', + version: '1', }, { label: 'PinkRoccadeV2', value: 'PinkRoccadeV2', + version: '2', }, { label: 'VrijBRP', value: 'VrijBRP', + version: '1', }, { label: 'WeAreFrank', value: 'WeAreFrank', + version: '2', }, ]; diff --git a/resources/js/blocks/personal-data-table/personal-data-row/edit.js b/resources/js/blocks/personal-data-table/personal-data-row/edit.js index 6edeaf8..68f07c0 100644 --- a/resources/js/blocks/personal-data-table/personal-data-row/edit.js +++ b/resources/js/blocks/personal-data-table/personal-data-row/edit.js @@ -38,6 +38,27 @@ export default function Edit( { attributes, setAttributes, clientId } ) { } }, [ blockParents, setAttributes ] ); + const supplierVersion = supplierOptions.find( + ( option ) => option.value === selectedSupplier.value + )?.version; + + // Only show the options of the API version of the supplier, the selected option is kept to avoid an empty select. + const availablePersonalDataOptions = personalDataOptions + .filter( + ( option ) => + ! option.version || + ! supplierVersion || + option.version === supplierVersion || + option.value === selectedOption.value + ) + .map( ( { label, value, disabled } ) => ( { label, value, disabled } ) ); + + // Labels saved by previous versions could contain a version suffix, e.g. 'Straat (V2)'. + const selectedOptionLabel = ( selectedOption.label ?? '' ).replace( + /\s*\(V\d+\)$/, + '' + ); + const handleSupplierChange = ( value ) => { const selectedSupplierData = { value, @@ -102,7 +123,7 @@ export default function Edit( { attributes, setAttributes, clientId } ) { ); @@ -165,11 +186,11 @@ export default function Edit( { attributes, setAttributes, clientId } ) {

{ isChildOfTable && (
-

{ selectedOption.label }

+

{ selectedOptionLabel }

) }
- [{ uncapitalize( selectedOption.label ) }] + [{ uncapitalize( selectedOptionLabel ) }]
diff --git a/src/PrefillGravityForms/Console/MigrateSupplierCommand.php b/src/PrefillGravityForms/Console/MigrateSupplierCommand.php new file mode 100644 index 0000000..e24e831 --- /dev/null +++ b/src/PrefillGravityForms/Console/MigrateSupplierCommand.php @@ -0,0 +1,153 @@ + + * : The V1 supplier to migrate from. + * --- + * options: + * - enable-u + * - pink-roccade + * - vrij-brp + * --- + * + * + * : The V2 supplier to migrate to. + * --- + * options: + * - enable-u-v2 + * - pink-roccade-v2 + * --- + * + * [--dry-run] + * : Only report what would be migrated. + * + * [--yes] + * : Do not ask for confirmation. + * + * ## EXAMPLES + * + * wp prefill migrate-supplier enable-u enable-u-v2 --dry-run + * wp prefill migrate-supplier vrij-brp enable-u-v2 --yes + */ + public function __invoke(array $args, array $assocArgs): void + { + $from = (string) ($args[0] ?? ''); + $to = (string) ($args[1] ?? ''); + $dryRun = (bool) \WP_CLI\Utils\get_flag_value($assocArgs, 'dry-run', false); + $service = new SupplierMigrationService(); + + if (! $service->canMigrate($from, $to)) { + WP_CLI::error(sprintf('There is no migration available from supplier "%s" to "%s".', $from, $to)); + } + + try { + $report = $service->migrate($from, $to, true); + } catch (Throwable $e) { + WP_CLI::error($e->getMessage()); + + return; // Unreachable: WP_CLI::error() exits; keeps static analysis happy. + } + + $this->printReport($report); + + if (0 === count($report['forms']) + count($report['posts'])) { + $this->warnSkippedForms($report); + WP_CLI::success(sprintf('Nothing to migrate for supplier "%s".', $from)); + + return; + } + + if ($dryRun) { + WP_CLI::success('Dry run, nothing has been changed.'); + + return; + } + + WP_CLI::confirm(sprintf('Migrate the forms and blocks above from "%s" to "%s"?', $report['from'], $report['to']), $assocArgs); + + try { + $report = $service->migrate($from, $to); + } catch (Throwable $e) { + WP_CLI::error($e->getMessage()); + } + + WP_CLI::success(sprintf( + 'Migrated %d form(s) and %d post(s) from "%s" to "%s".', + count($report['forms']), + count($report['posts']), + $report['from'], + $report['to'] + )); + + $this->warnSkippedForms($report); + } + + private function warnSkippedForms(array $report): void + { + if (count($report['skipped_forms'])) { + WP_CLI::warning(sprintf('%d form(s) have been skipped, these need to be migrated manually.', count($report['skipped_forms']))); + } + } + + private function printReport(array $report): void + { + $items = []; + + foreach ($report['forms'] as $id => $form) { + $items[] = [ + 'type' => 'form', + 'id' => $id, + 'title' => $form['title'], + 'migrated' => $form['migrated'], + 'unmapped' => count($form['unmapped']), + 'note' => '', + ]; + } + + foreach ($report['skipped_forms'] as $id => $form) { + $items[] = ['type' => 'form', 'id' => $id, 'title' => $form['title'], 'migrated' => 0, 'unmapped' => 0, 'note' => 'skipped: ' . $form['reason']]; + } + + foreach ($report['posts'] as $id => $post) { + $items[] = ['type' => 'post', 'id' => $id, 'title' => $post['title'], 'migrated' => $post['migrated'], 'unmapped' => 0, 'note' => '']; + } + + if (count($items)) { + \WP_CLI\Utils\format_items('table', $items, ['type', 'id', 'title', 'migrated', 'unmapped', 'note']); + } + + $unmapped = []; + + foreach ($report['forms'] as $id => $form) { + foreach ($form['unmapped'] as $fieldId => $field) { + $unmapped[] = ['form' => $id, 'field' => $fieldId, 'label' => $field['label'], 'value' => $field['value']]; + } + } + + if (count($unmapped)) { + WP_CLI::log(''); + WP_CLI::log('Fields without a V2 counterpart, these need to be linked manually:'); + \WP_CLI\Utils\format_items('table', $unmapped, ['form', 'field', 'label', 'value']); + } + } +} diff --git a/src/PrefillGravityForms/GravityForms/GravityFormsAddon.php b/src/PrefillGravityForms/GravityForms/GravityFormsAddon.php index 5ca2ba9..0826cb1 100644 --- a/src/PrefillGravityForms/GravityForms/GravityFormsAddon.php +++ b/src/PrefillGravityForms/GravityForms/GravityFormsAddon.php @@ -58,6 +58,11 @@ public static function get_instance(): self return self::$_instance; } + /** + * HTML message about the supplier migration, shown with the settings saved message. + */ + private string $supplierMigrationMessage = ''; + /** * Return the plugin's icon for the plugin/form settings menu. * @@ -140,6 +145,12 @@ public function plugin_settings_fields(): array ]; }, array_values(config('suppliers.mapping', [])))), ], + [ + // Set by JavaScript when the user confirms the migration of forms and blocks to the V2 supplier. + 'type' => 'hidden', + 'name' => SupplierMigrationSettings::SETTING_CONFIRMED, + 'default_value' => '', + ], [ 'label' => esc_html__('Gebruik API authenticatie', 'prefill-gravity-forms'), 'description' => esc_html__('Deze authenticatie zal gebruikt worden naast de gebruikelijke authenticatie middels certificaten.', 'prefill-gravity-forms'), @@ -303,6 +314,73 @@ public function plugin_settings_fields(): array ]; } + /** + * @since NEXT + */ + public function init_admin() + { + parent::init_admin(); + + add_action('admin_enqueue_scripts', $this->enqueueSupplierMigrationScript(...)); + } + + /** + * @since NEXT + */ + public function plugin_settings_init() + { + parent::plugin_settings_init(); + + $renderer = $this->get_settings_renderer(); + + if (! is_object($renderer) || ! method_exists($renderer, 'set_postback_message_callback')) { + return; + } + + $renderer->set_postback_message_callback(function ($message) { + return $message . $this->supplierMigrationMessage; + }); + } + + /** + * Migrates the forms and blocks when the supplier is changed from V1 to V2 and the user confirmed this. + * + * @param array $settings + * @since NEXT + */ + public function update_plugin_settings($settings) + { + $confirmed = '1' === (string) ($settings[SupplierMigrationSettings::SETTING_CONFIRMED] ?? ''); + unset($settings[SupplierMigrationSettings::SETTING_CONFIRMED]); + + $previousSettings = $this->get_plugin_settings(); + $previousSupplier = is_array($previousSettings) ? (string) ($previousSettings[SupplierMigrationSettings::SETTING_SUPPLIER] ?? '') : ''; + + parent::update_plugin_settings($settings); + + $this->supplierMigrationMessage = (new SupplierMigrationSettings())->handleSupplierChange( + $previousSupplier, + (string) ($settings[SupplierMigrationSettings::SETTING_SUPPLIER] ?? ''), + $confirmed + ); + } + + /** + * @since NEXT + */ + private function enqueueSupplierMigrationScript(): void + { + if (! $this->is_plugin_settings($this->get_slug())) { + return; + } + + $settings = $this->get_plugin_settings(); + + (new SupplierMigrationSettings())->enqueueConfirmScript( + is_array($settings) ? (string) ($settings[SupplierMigrationSettings::SETTING_SUPPLIER] ?? '') : '' + ); + } + /** * Format the list of certificates for the selectbox. */ diff --git a/src/PrefillGravityForms/GravityForms/SupplierMigrationSettings.php b/src/PrefillGravityForms/GravityForms/SupplierMigrationSettings.php new file mode 100644 index 0000000..bf4633d --- /dev/null +++ b/src/PrefillGravityForms/GravityForms/SupplierMigrationSettings.php @@ -0,0 +1,215 @@ +service = $service ?? new SupplierMigrationService(); + } + + /** + * Migrates the forms and blocks when the supplier changed and the user confirmed the migration. + * Without confirmation the user is informed about the forms and blocks that are still linked to the V1 supplier. + * + * Returns a HTML message which is shown with the settings saved message. + */ + public function handleSupplierChange(string $from, string $to, bool $confirmed): string + { + if ($from === $to) { + return ''; + } + + $migration = $this->service->findMigrationForGlobalSupplierChange($from, $to); + + if (null === $migration) { + return ''; + } + + try { + $report = $this->service->migrate($migration[0], $migration[1], ! $confirmed); + } catch (Throwable $e) { + $this->logException(new \Exception(sprintf('Supplier migration failed: %s', $e->getMessage()), 500, $e)); + + return $this->paragraph(sprintf( + esc_html__('Het omzetten van de formulieren en blokken naar %s is mislukt: %s', 'prefill-gravity-forms'), + esc_html($to), + esc_html($e->getMessage()) + )); + } + + return $confirmed ? $this->formatMigrated($report) : $this->formatNotMigrated($report); + } + + /** + * Enqueues the script which asks for confirmation when the supplier is changed from the current V1 supplier + * to a V2 supplier, see resources/js/admin/supplier-migration.js. + * + * The linked forms and blocks are not counted here, as that requires loading all forms and searching all posts + * on every load of the settings page. When nothing is linked, confirming the migration simply changes nothing. + */ + public function enqueueConfirmScript(string $currentSupplier): void + { + $from = $this->service->supplierByLabel($currentSupplier, $this->service->sourceSuppliers()); + $targets = $this->service->targetSuppliers(); + + if (null === $from || ! count($targets)) { + return; + } + + $assetPath = PG_ROOT_PATH . '/build/supplier-migration.asset.php'; + $asset = file_exists($assetPath) ? require $assetPath : ['dependencies' => [], 'version' => PG_VERSION]; + + wp_enqueue_script( + self::SCRIPT_HANDLE, + plugins_url('build/supplier-migration.js', PG_ROOT_PATH . '/prefill-gravity-forms.php'), + $asset['dependencies'], + $asset['version'], + true + ); + + $message = sprintf( + // The %s placeholder (escaped as %%s) is replaced by the selected V2 supplier in JavaScript. + __("Wil je de formulieren en pagina's met persoonsgegevens die gekoppeld zijn aan %1\$s automatisch omzetten naar %%s? De gekoppelde velden worden dan vervangen door de V2-velden. Velden zonder V2-tegenhanger worden ontkoppeld en na het opslaan getoond.\n\nOK: omzetten en instellingen opslaan.\nAnnuleren: alleen de instellingen opslaan.", 'prefill-gravity-forms'), + $currentSupplier + ); + + wp_add_inline_script( + self::SCRIPT_HANDLE, + sprintf('window.owcPrefillSupplierMigration = %s;', wp_json_encode([ + 'supplierInput' => self::INPUT_PREFIX . self::SETTING_SUPPLIER, + 'confirmedInput' => self::INPUT_PREFIX . self::SETTING_CONFIRMED, + 'targets' => array_map($this->service->supplierLabel(...), $targets), + 'message' => $message, + ])), + 'before' + ); + } + + private function formatMigrated(array $report): string + { + $from = $this->service->supplierLabel($report['from']); + $to = $this->service->supplierLabel($report['to']); + + $parts = array_filter([ + count($report['forms']) ? sprintf( + esc_html__('%1$d formulier(en) met %2$d gekoppelde velden', 'prefill-gravity-forms'), + count($report['forms']), + array_sum(array_column($report['forms'], 'migrated')) + ) : '', + count($report['posts']) ? sprintf( + esc_html__('%1$d pagina(\'s) met %2$d persoonsgegevens-blokken', 'prefill-gravity-forms'), + count($report['posts']), + array_sum(array_column($report['posts'], 'migrated')) + ) : '', + ]); + + $html = count($parts) ? $this->paragraph(sprintf( + esc_html__('%1$s zijn omgezet van %2$s naar %3$s.', 'prefill-gravity-forms'), + implode(esc_html__(' en ', 'prefill-gravity-forms'), $parts), + esc_html($from), + esc_html($to) + )) : ''; + + return $html . $this->formatUnmapped($report) . $this->formatSkipped($report); + } + + private function formatNotMigrated(array $report): string + { + $forms = count($report['forms']) + count($report['skipped_forms']); + $posts = count($report['posts']); + + if (0 === $forms + $posts) { + return ''; + } + + return $this->paragraph(sprintf( + esc_html__('Let op: %1$d formulier(en) en %2$d pagina(\'s) met persoonsgegevens zijn nog gekoppeld aan %3$s en zijn niet omgezet. Pas deze handmatig aan of gebruik het WP-CLI commando %4$s.', 'prefill-gravity-forms'), + $forms, + $posts, + esc_html($this->service->supplierLabel($report['from'])), + sprintf('wp prefill migrate-supplier %s %s', esc_html($report['from']), esc_html($report['to'])) + )); + } + + private function formatUnmapped(array $report): string + { + $items = []; + + foreach ($report['forms'] as $formId => $form) { + foreach ($form['unmapped'] as $fieldId => $field) { + $items[] = sprintf( + '%s – %s %s', + $this->formLink((int) $formId, $form['title']), + esc_html('' !== $field['label'] ? $field['label'] : sprintf(__('Veld %d', 'prefill-gravity-forms'), $fieldId)), + esc_html($field['value']) + ); + } + } + + if (! count($items)) { + return ''; + } + + return $this->paragraph(esc_html__('De volgende velden hebben geen tegenhanger in V2 en moeten handmatig opnieuw gekoppeld worden:', 'prefill-gravity-forms')) + . $this->list($items); + } + + private function formatSkipped(array $report): string + { + $items = []; + + foreach ($report['skipped_forms'] as $formId => $form) { + $items[] = sprintf('%s – %s', $this->formLink((int) $formId, $form['title']), esc_html($form['reason'])); + } + + if (! count($items)) { + return ''; + } + + return $this->paragraph(esc_html__('De volgende formulieren zijn overgeslagen en moeten handmatig aangepast worden:', 'prefill-gravity-forms')) + . $this->list($items); + } + + private function formLink(int $formId, string $title): string + { + return sprintf( + '%s (ID %d)', + esc_url(admin_url(sprintf('admin.php?page=gf_edit_forms&id=%d', $formId))), + esc_html($title), + $formId + ); + } + + private function paragraph(string $html): string + { + return sprintf('

%s

', $html); + } + + private function list(array $items): string + { + return sprintf('
  • %s
', implode('
  • ', $items)); + } +} diff --git a/src/PrefillGravityForms/Providers/GravityFormsServiceProvider.php b/src/PrefillGravityForms/Providers/GravityFormsServiceProvider.php index 3ee6575..7b71fe2 100644 --- a/src/PrefillGravityForms/Providers/GravityFormsServiceProvider.php +++ b/src/PrefillGravityForms/Providers/GravityFormsServiceProvider.php @@ -5,6 +5,7 @@ use GF_Fields; use GFAddOn; use GFForms; +use OWC\PrefillGravityForms\Console\MigrateSupplierCommand; use function OWC\PrefillGravityForms\Foundation\Helpers\config; use function OWC\PrefillGravityForms\Foundation\Helpers\view; use OWC\PrefillGravityForms\Foundation\ServiceProvider; @@ -12,6 +13,7 @@ use OWC\PrefillGravityForms\GravityForms\GravityFormsAddon; use OWC\PrefillGravityForms\GravityForms\GravityFormsFieldSettings; use OWC\PrefillGravityForms\GravityForms\GravityFormsFormSettings; +use WP_CLI; class GravityFormsServiceProvider extends ServiceProvider { @@ -20,6 +22,19 @@ public function register(): void $this->registerHooks(); $this->registerFieldsWithTabs(); $this->registerSettingsAddon(); + $this->registerCommands(); + } + + /** + * @since NEXT + */ + protected function registerCommands(): void + { + if (! defined('WP_CLI') || ! WP_CLI) { + return; + } + + WP_CLI::add_command('prefill migrate-supplier', MigrateSupplierCommand::class); } protected function registerHooks(): void diff --git a/src/PrefillGravityForms/Services/SupplierMigrationService.php b/src/PrefillGravityForms/Services/SupplierMigrationService.php new file mode 100644 index 0000000..d6ad768 --- /dev/null +++ b/src/PrefillGravityForms/Services/SupplierMigrationService.php @@ -0,0 +1,313 @@ +sources()); + } + + /** + * Returns the keys of the V2 suppliers that can be migrated to, e.g. 'enable-u-v2'. + */ + public function targetSuppliers(): array + { + return array_keys($this->targets()); + } + + public function canMigrate(string $from, string $to): bool + { + return isset($this->sources()[$from], $this->targets()[$to]); + } + + /** + * Find the migration for a change of the global supplier setting, which uses the controller names, e.g. + * 'VrijBRP' to 'EnableUV2'. Returns the supplier keys, e.g. ['vrij-brp', 'enable-u-v2'], or null when there is none. + * + * @return array{0: string, 1: string}|null + */ + public function findMigrationForGlobalSupplierChange(string $from, string $to): ?array + { + $from = $this->supplierByLabel($from, $this->sourceSuppliers()); + $to = $this->supplierByLabel($to, $this->targetSuppliers()); + + return null === $from || null === $to ? null : [$from, $to]; + } + + /** + * Returns the key of a supplier by its controller name, e.g. 'EnableU' becomes 'enable-u'. + */ + public function supplierByLabel(string $label, ?array $suppliers = null): ?string + { + foreach ($suppliers ?? array_keys((array) config('suppliers.mapping', [])) as $supplier) { + if ('' !== $label && $this->supplierLabel($supplier) === $label) { + return $supplier; + } + } + + return null; + } + + /** + * Returns the controller name of a supplier key, e.g. 'enable-u' becomes 'EnableU'. + */ + public function supplierLabel(string $supplier): string + { + $mapping = config('suppliers.mapping', []); + + return is_array($mapping) ? (string) ($mapping[$supplier] ?? '') : ''; + } + + /** + * Migrate (or with $dryRun only report) all forms and blocks linked to the V1 supplier $from to the V2 supplier $to. + * + * @return array{ + * from: string, + * to: string, + * forms: array}>, + * skipped_forms: array, + * posts: array + * } + */ + public function migrate(string $from, string $to, bool $dryRun = false): array + { + if (! $this->canMigrate($from, $to)) { + throw new \InvalidArgumentException(sprintf('There is no migration available from supplier "%s" to "%s".', $from, $to)); + } + + return [ + 'from' => $from, + 'to' => $to, + ...$this->migrateForms($from, $to, $dryRun), + 'posts' => $this->migrateBlocks($from, $to, $dryRun), + ]; + } + + private function sources(): array + { + $sources = config('supplier-migrations.sources', []); + + return is_array($sources) ? $sources : []; + } + + private function targets(): array + { + $targets = config('supplier-migrations.targets', []); + + return is_array($targets) ? $targets : []; + } + + private function migrateForms(string $supplier, string $to, bool $dryRun): array + { + $report = ['forms' => [], 'skipped_forms' => []]; + + if (! class_exists('GFAPI')) { + return $report; + } + + // Include inactive and trashed forms, these could be restored later on. + foreach (GFAPI::get_forms(null, null) as $form) { + if (($form[self::FORM_SETTING_SUPPLIER] ?? '') !== $supplier) { + continue; + } + + $formId = (int) $form['id']; + $title = (string) ($form['title'] ?? ''); + + // Theme mapping files contain custom options which can not be mapped automatically. + $themeMapping = (string) ($form[self::FORM_SETTING_THEME_MAPPING] ?? '0'); + + if ('0' !== $themeMapping && '' !== $themeMapping) { + $report['skipped_forms'][$formId] = [ + 'title' => $title, + 'reason' => sprintf(__('gebruikt mappingbestand "%s" uit het thema', 'prefill-gravity-forms'), basename($themeMapping)), + ]; + + continue; + } + + $result = $this->migrateFormFields($form, $this->sources()[$supplier], ! $dryRun); + $report['forms'][$formId] = ['title' => $title] + $result; + + if ($dryRun) { + continue; + } + + $form[self::FORM_SETTING_SUPPLIER_LEGACY] = $supplier; + $form[self::FORM_SETTING_SUPPLIER] = $to; + + $updated = GFAPI::update_form($form); + + if (is_wp_error($updated)) { + unset($report['forms'][$formId]); + $report['skipped_forms'][$formId] = ['title' => $title, 'reason' => $updated->get_error_message()]; + } + } + + return $report; + } + + /** + * Maps the linked field values of the form fields, the fields are objects so the form is modified by reference. + * When $apply is false the fields are left untouched, so a dry run does not alter forms cached by Gravity Forms. + */ + private function migrateFormFields(array $form, array $fieldMapping, bool $apply): array + { + $result = ['migrated' => 0, 'unmapped' => []]; + + foreach ($form['fields'] ?? [] as $field) { + $value = $field->linkedFieldValue ?? ''; + + if (! is_string($value) || '' === $value) { + continue; + } + + $newValue = $fieldMapping[$value] ?? null; + + $isMapped = is_string($newValue) && '' !== $newValue; + + if ($isMapped) { + $result['migrated']++; + } else { + $result['unmapped'][(int) $field->id] = [ + 'label' => (string) ($field->label ?? ''), + 'value' => $value, + ]; + } + + if (! $apply) { + continue; + } + + /** + * Keep the original value so it is clear what the field was linked to. Fields without a V2 counterpart + * are unlinked, the V1 value does not exist in V2 and would otherwise be used to prefill the field. + */ + $field->{self::FIELD_LEGACY_PROPERTY} = $value; + $field->linkedFieldValue = $isMapped ? $newValue : ''; + } + + return $result; + } + + private function migrateBlocks(string $supplier, string $target, bool $dryRun): array + { + global $wpdb; + + $from = $this->supplierLabel($supplier); + $to = $this->supplierLabel($target); + + if ('' === $from || '' === $to) { + return []; + } + + $posts = $wpdb->get_results($wpdb->prepare( + "SELECT ID, post_title, post_content FROM {$wpdb->posts} WHERE post_type != 'revision' AND post_content LIKE %s", + '%' . $wpdb->esc_like('/s', preg_quote(self::BLOCK_NAME, '/')); + $optionMapping = config('supplier-migrations.block_options', []); + + return (string) preg_replace_callback($pattern, function (array $matches) use ($from, $to, $optionMapping, &$migrated): string { + // Decode as objects to preserve empty objects when encoding again. + $attributes = json_decode($matches[1]); + + if (! $attributes instanceof stdClass) { + return $matches[0]; + } + + $supplier = $attributes->selectedSupplier->value ?? ''; + $option = $attributes->selectedOption->value ?? ''; + $mappedOption = is_string($option) ? ($optionMapping[$option] ?? null) : null; + + // Blocks already linked to the V2 supplier only need their option mapped, e.g. after an earlier migration. + if ($supplier !== $from && ($supplier !== $to['value'] || ! is_array($mappedOption))) { + return $matches[0]; + } + + $attributes->selectedSupplier = (object) $to; + + if (is_array($mappedOption)) { + $attributes->selectedOption = (object) $mappedOption; + } + + $migrated++; + + return sprintf( + '', + self::BLOCK_NAME, + serialize_block_attributes((array) $attributes), + empty($matches[2]) ? '' : '/' + ); + }, $content); + } +} diff --git a/src/PrefillGravityForms/SupplierMigration/V1ToV2FieldMapping.php b/src/PrefillGravityForms/SupplierMigration/V1ToV2FieldMapping.php new file mode 100644 index 0000000..09aaad8 --- /dev/null +++ b/src/PrefillGravityForms/SupplierMigration/V1ToV2FieldMapping.php @@ -0,0 +1,103 @@ + 'burgerservicenummer', + 'aNummer' => 'aNummer', + 'geslachtsaanduiding' => 'geslacht.omschrijving', + 'leeftijd' => 'leeftijd', + + 'naam.geslachtsnaam' => 'naam.geslachtsnaam', + 'naam.voorletters' => 'naam.voorletters', + 'naam.voornamen' => 'naam.voornamen', + 'naam.voorvoegsel' => 'naam.voorvoegsel', + 'naam.aanschrijfwijze' => 'naam.aanschrijfwijze', + 'naam.aanduidingNaamgebruik' => 'naam.aanduidingNaamgebruik.omschrijving', + + 'nationaliteiten.datumIngangGeldigheid.datum' => 'nationaliteiten.0.datumIngangGeldigheid.datum', + 'nationaliteiten.datumIngangGeldigheid.jaar' => null, + 'nationaliteiten.datumIngangGeldigheid.maand' => null, + 'nationaliteiten.datumIngangGeldigheid.dag' => null, + 'nationaliteiten.nationaliteit.omschrijving' => 'nationaliteiten.0.nationaliteit.omschrijving', + 'nationaliteiten.nationaliteit.code' => 'nationaliteiten.0.nationaliteit.code', + + 'geboorte.datum.datum' => 'geboorte.datum.datum', + 'geboorte.land.omschrijving' => 'geboorte.land.omschrijving', + 'geboorte.plaats.omschrijving' => 'geboorte.plaats.omschrijving', + + 'verblijfplaats.huisnummer' => 'verblijfplaats.verblijfadres.huisnummer', + 'verblijfplaats.huisletter' => 'verblijfplaats.verblijfadres.huisletter', + 'verblijfplaats.postcode' => 'verblijfplaats.verblijfadres.postcode', + 'verblijfplaats.adresregel1' => 'adressering.adresregel1', + 'verblijfplaats.adresregel2' => 'adressering.adresregel2', + 'verblijfplaats.gemeenteVanInschrijving.omschrijving' => 'gemeenteVanInschrijving.omschrijving', + 'verblijfplaats.gemeenteVanInschrijving.code' => 'gemeenteVanInschrijving.code', + + '_embedded.partners.soortVerbintenis' => 'partners.0.soortVerbintenis.omschrijving', + '_embedded.partners.naam.geslachtsnaam' => 'partners.0.naam.geslachtsnaam', + '_embedded.partners.naam.voornamen' => 'partners.0.naam.voornamen', + ]; + + foreach (['ouders.0', 'ouders.1', 'kinderen.*'] as $relative) { + $fields += self::prefix(self::relative(), "_embedded.{$relative}", $relative); + + // Children do not have a gender attribute in the V2 mapping options. + $fields["_embedded.{$relative}.geslachtsaanduiding"] = 'kinderen.*' === $relative ? null : "{$relative}.geslacht.omschrijving"; + } + + return $fields; + } + + /** + * Attributes of an embedded relative (parent, child or partner), which share the same structure in V1. + */ + public static function relative(): array + { + return [ + 'burgerservicenummer' => null, + 'naam.geslachtsnaam' => 'naam.geslachtsnaam', + 'naam.voorletters' => 'naam.voorletters', + 'naam.voornamen' => 'naam.voornamen', + 'naam.voorvoegsel' => 'naam.voorvoegsel', + 'geboorte.datum.datum' => 'geboorte.datum.datum', + 'geboorte.datum.dag' => null, + 'geboorte.datum.maand' => null, + 'geboorte.plaats.code' => null, + 'geboorte.plaats.omschrijving' => 'geboorte.plaats.omschrijving', + 'geboorte.land.code' => null, + 'geboorte.land.omschrijving' => 'geboorte.land.omschrijving', + ]; + } + + /** + * Prefix the V1 keys and the V2 values of a mapping, values without a V2 counterpart remain null. + */ + public static function prefix(array $mapping, string $v1Prefix, string $v2Prefix): array + { + $prefixed = []; + + foreach ($mapping as $v1 => $v2) { + $prefixed["{$v1Prefix}.{$v1}"] = null === $v2 ? null : "{$v2Prefix}.{$v2}"; + } + + return $prefixed; + } +} diff --git a/webpack.config.js b/webpack.config.js index f79f692..7283414 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ module.exports = { ...entries, icons: './resources/scss/icons.scss', style: './resources/scss/index.scss', + 'supplier-migration': './resources/js/admin/supplier-migration.js', }; }, };