From 8c914a52c0c78349e1b5bfe98ecf9c26c6a6b437 Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Thu, 17 Jul 2025 12:49:37 +0530 Subject: [PATCH 1/7] Add initial form variations(templates). --- ...s-mailchimp-list-subscribe-form-blocks.php | 2 + includes/blocks/mailchimp/edit.js | 15 +-- includes/blocks/mailchimp/editor.css | 35 ++++++ includes/blocks/mailchimp/index.js | 2 + includes/blocks/mailchimp/variation-picker.js | 93 ++++++++++++++ includes/blocks/mailchimp/variations.js | 117 ++++++++++++++++++ 6 files changed, 257 insertions(+), 7 deletions(-) create mode 100644 includes/blocks/mailchimp/variation-picker.js create mode 100644 includes/blocks/mailchimp/variations.js diff --git a/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php b/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php index 45496dca..1e63983b 100644 --- a/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php +++ b/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php @@ -78,6 +78,8 @@ public function register_blocks() { 'double_opt_in' => (bool) get_option( 'mc_double_optin', true ), 'merge_fields_visibility' => $merge_fields_visibility, 'interest_groups_visibility' => $interest_groups_visibility, + 'merge_fields' => $merge_fields, + 'interest_groups' => $interest_groups, ); $data = 'window.mailchimp_sf_block_data = ' . wp_json_encode( $data ); wp_add_inline_script( 'mailchimp-mailchimp-editor-script', $data, 'before' ); diff --git a/includes/blocks/mailchimp/edit.js b/includes/blocks/mailchimp/edit.js index a45e5010..75fbbc5e 100644 --- a/includes/blocks/mailchimp/edit.js +++ b/includes/blocks/mailchimp/edit.js @@ -18,6 +18,7 @@ import apiFetch from '@wordpress/api-fetch'; import { useDispatch, useSelect } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; import Icon from './icon'; +import { VariationPicker } from './variation-picker'; const SelectListPlaceholder = () => { return ( @@ -161,19 +162,14 @@ export const BlockEdit = (props) => { tag: field.tag, label: field.name, type: field.type, - visible: - (field.required || - merge_fields_visibility?.[field.tag] === 'on') && - field.public, + visible: field.required, // Keep newly added fields hidden by default, except for required fields. }), ); const newGroupBlocks = newFormGroups.map((group) => createBlock('mailchimp/mailchimp-audience-group', { id: group.id, label: group.title, - visible: - interest_groups_visibility?.[group.id] === 'on' && - group.type !== 'hidden', + visible: false, // Keep newly added groups hidden by default. }), ); @@ -279,6 +275,11 @@ export const BlockEdit = (props) => { ); } + // Display the variation picker if there are no innerBlocks. + if (innerBlocks.length === 0) { + return ; + } + // Create a template for innerBlocks based on list data and visibility settings. const templateFields = listData?.merge_fields?.map((field) => [ diff --git a/includes/blocks/mailchimp/editor.css b/includes/blocks/mailchimp/editor.css index 31229236..d37c65c8 100644 --- a/includes/blocks/mailchimp/editor.css +++ b/includes/blocks/mailchimp/editor.css @@ -151,3 +151,38 @@ select.mc_select { width:100%; margin-top: 1em; } + +.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker .components-placeholder__fieldset .block-editor-block-variation-picker__variations { + gap: 20px 0; + justify-content: center; + margin: 16px auto; + max-width: 560px; +} + +.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker .components-placeholder__fieldset .block-editor-block-variation-picker__variations>li { + flex: 0; + margin: 0; + max-width: none; + padding: 0 4px; + text-align: center; + width: unset +} + +.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker .components-placeholder__fieldset .block-editor-block-variation-picker__variations .block-editor-block-variation-picker__variation { + margin-left: 4px; + margin-right: 4px; + padding: 22px +} + +.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker .components-placeholder__fieldset .block-editor-block-variation-picker__variations .block-editor-block-variation-picker__variation span.dashicon::before { + font-size: 24px; +} + +.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker .components-placeholder__fieldset .block-editor-block-variation-picker__variations .block-editor-block-variation-picker__variation svg { + height: auto; + width: 24px +} + +.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker .components-placeholder__fieldset .block-editor-block-variation-picker__variations .block-editor-block-variation-picker__variation-label { + margin-right: 0 +} diff --git a/includes/blocks/mailchimp/index.js b/includes/blocks/mailchimp/index.js index 134b4879..bbbdec13 100644 --- a/includes/blocks/mailchimp/index.js +++ b/includes/blocks/mailchimp/index.js @@ -4,9 +4,11 @@ import { InnerBlocks } from '@wordpress/block-editor'; import { BlockEdit } from './edit'; import metadata from './block.json'; import Icon from './icon'; +import { variations } from './variations'; registerBlockType(metadata, { icon: Icon, + variations, transforms: { from: [ { diff --git a/includes/blocks/mailchimp/variation-picker.js b/includes/blocks/mailchimp/variation-picker.js new file mode 100644 index 00000000..8fc87c85 --- /dev/null +++ b/includes/blocks/mailchimp/variation-picker.js @@ -0,0 +1,93 @@ +import { + __experimentalBlockVariationPicker as BlockVariationPicker, // eslint-disable-line @wordpress/no-unsafe-wp-apis + store as blockEditorStore, + useBlockProps, +} from '@wordpress/block-editor'; +import { createBlocksFromInnerBlocksTemplate, store as blocksStore } from '@wordpress/blocks'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { formFields, formFieldTitles } from './variations'; + +const getMissingFields = (variation) => { + const variationName = variation.name; + const variationFields = formFields[variationName] || []; + const formFieldsTags = variation.innerBlocks + .filter((block) => block[0] === 'mailchimp/mailchimp-form-field') + .map((block) => { + const [, attributes] = block; + const { tag } = attributes; + return tag; + }); + + return variationFields.filter((field) => !formFieldsTags.includes(field)); +}; + +export const VariationPicker = ({ name, setAttributes, clientId }) => { + const { blockType, defaultVariation, variations } = useSelect( + (select) => { + const { getBlockVariations, getBlockType, getDefaultBlockVariation } = + select(blocksStore); + + return { + blockType: getBlockType(name), + defaultVariation: getDefaultBlockVariation(name, 'block'), + variations: getBlockVariations(name, 'block'), + }; + }, + [name], + ); + const { replaceInnerBlocks } = useDispatch(blockEditorStore); + const { createNotice } = useDispatch('core/notices'); + const blockProps = useBlockProps(); + + return ( +
+ { + if (nextVariation.attributes) { + setAttributes(nextVariation.attributes); + } + + if (nextVariation.innerBlocks) { + const missingFields = getMissingFields(nextVariation); + + replaceInnerBlocks( + clientId, + createBlocksFromInnerBlocksTemplate(nextVariation.innerBlocks), + ); + + // Add a notice if there are missing fields from the selected form template. + if (missingFields.length > 0) { + createNotice( + 'warning', + sprintf( + _n( + "%s form field is missing from the selected form template. Please create this field in the Mailchimp dashboard, then click the 'Fetch list settings' button on the plugin settings page to update the list and include the missing field.", + "Some form fields are missing from the selected form template: %s. Please create these fields in the Mailchimp dashboard, then click the 'Fetch list settings' button on the plugin settings page to update the list and include the missing fields.", + missingFields.length, + 'mailchimp', + ), + missingFields + .map( + (field) => + `${formFieldTitles[field] || field} (${field})`, + ) + .join(', '), + ), + { + id: 'mailchimp-form-template-field-notice', + isDismissible: true, + }, + ); + } + } + }} + allowSkip + /> +
+ ); +}; diff --git a/includes/blocks/mailchimp/variations.js b/includes/blocks/mailchimp/variations.js new file mode 100644 index 00000000..657d4038 --- /dev/null +++ b/includes/blocks/mailchimp/variations.js @@ -0,0 +1,117 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const { mailchimp_sf_block_data } = window; +const { + merge_fields = [], + interest_groups = [], + merge_fields_visibility = {}, +} = mailchimp_sf_block_data; + +/** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */ + +const groupInnerBlocks = interest_groups.map((group) => [ + 'mailchimp/mailchimp-audience-group', + { + id: group.id, + label: group.title, + visible: false, + }, +]); + +const prepareInnerBlocks = (merge_fields = [], fields = []) => { + return [...merge_fields] + .sort((a, b) => { + const aIndex = fields.indexOf(a.tag); + const bIndex = fields.indexOf(b.tag); + return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex); + }) + .map((field) => { + let visible = + field.required || + fields.includes(field.tag) || + fields.includes(field.type?.toUpperCase()); + if (fields.length === 0) { + visible = field.required || merge_fields_visibility?.[field.tag] === 'on'; + } + return [ + 'mailchimp/mailchimp-form-field', + { + tag: field.tag, + label: field.name, + type: field.type, + visible, + }, + ]; + }); +}; + +export const formFields = { + 'email-only-form': ['EMAIL'], + 'name-and-email-form': ['FNAME', 'LNAME', 'EMAIL'], + 'contact-form': ['FNAME', 'LNAME', 'EMAIL', 'PHONE', 'ADDRESS'], + default: [], +}; + +export const formFieldTitles = { + FNAME: __('First Name', 'mailchimp'), + LNAME: __('Last Name', 'mailchimp'), + EMAIL: __('Email', 'mailchimp'), + PHONE: __('Phone', 'mailchimp'), + ADDRESS: __('Address', 'mailchimp'), +}; + +/** + * Template option choices for predefined columns layouts. + * + * @type {WPBlockVariation[]} + */ +export const variations = [ + { + name: 'email-only-form', + title: __('Quick Signup (Email Only)', 'mailchimp'), + description: __('A quick signup form with only an email field.', 'mailchimp'), + icon: 'email', + innerBlocks: [ + ...prepareInnerBlocks(merge_fields, formFields['email-only-form']), + ...groupInnerBlocks, + ], + scope: ['block'], + }, + { + name: 'name-and-email-form', + title: __('Personal Signup (Name and Email)', 'mailchimp'), + description: __('A personal signup form with only a name and email fields', 'mailchimp'), + icon: 'admin-users', + innerBlocks: [ + ...prepareInnerBlocks(merge_fields, formFields['name-and-email-form']), + ...groupInnerBlocks, + ], + scope: ['block'], + }, + { + name: 'contact-form', + title: __('Contact Form (Contact Details)', 'mailchimp'), + description: __( + 'A full contact details form with name, email, phone and address fields', + 'mailchimp', + ), + icon: 'id', + innerBlocks: [ + ...prepareInnerBlocks(merge_fields, formFields['contact-form']), + ...groupInnerBlocks, + ], + scope: ['block'], + }, + { + name: 'default', + title: __('Default Form (All Fields)', 'mailchimp'), + description: __('A default form, Fields based on settings.', 'mailchimp'), + icon: 'admin-settings', + isDefault: true, + innerBlocks: [...prepareInnerBlocks(merge_fields, formFields.default), ...groupInnerBlocks], + scope: ['block'], + }, +]; From f8a38be05a9491d6f820dc389c0f3df3f0ea942f Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Thu, 17 Jul 2025 15:51:51 +0530 Subject: [PATCH 2/7] Update form field visibility based on the previous list form while switching the list. --- includes/blocks/mailchimp/edit.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/includes/blocks/mailchimp/edit.js b/includes/blocks/mailchimp/edit.js index 75fbbc5e..158e126a 100644 --- a/includes/blocks/mailchimp/edit.js +++ b/includes/blocks/mailchimp/edit.js @@ -75,7 +75,10 @@ export const BlockEdit = (props) => { ); const exisingTags = innerBlocks.map((block) => block?.attributes?.tag).filter(Boolean); const exisingGroups = innerBlocks.map((block) => block?.attributes?.id).filter(Boolean); - const visibleFieldsCount = innerBlocks.filter((block) => block?.attributes?.visible).length; + const visibleFields = innerBlocks + .filter((block) => block?.attributes?.visible) + .map((block) => block?.attributes?.tag); + const visibleFieldsCount = visibleFields.length; const listOptions = []; // Check if selected list is not in the list of available lists. @@ -117,10 +120,18 @@ export const BlockEdit = (props) => { tag: field.tag, label: field.name, type: field.type, + /** + * Visibility logic: + * 1. If there are visible fields from the previous list, make the field visible if it's required or it's visible in the previous list form (Try to keep the same visibility as the previous list form). + * 2. If there are no visible fields from the previous list, make the field visible if it's required or it's public and the visibility setting is on in the global settings. + */ visible: - (field.required || - merge_fields_visibility?.[field.tag] === 'on') && - field.public, + field.required || + (visibleFields.length > 0 && + visibleFields.includes(field.tag)) || + (visibleFields.length === 0 && + merge_fields_visibility?.[field.tag] === 'on' && + field.public), }), ) || []; const listGroupsBlocks = @@ -128,9 +139,7 @@ export const BlockEdit = (props) => { createBlock('mailchimp/mailchimp-audience-group', { id: group.id, label: group.title, - visible: - interest_groups_visibility?.[group.id] === 'on' && - group.type !== 'hidden', + visible: false, // Keep the groups hidden by default. }), ) || []; replaceInnerBlocks(clientId, [...listFieldsBlocks, ...listGroupsBlocks], false); From ce052b991bac3f828bb9d706bd975f6b07e3cd04 Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Thu, 17 Jul 2025 18:46:13 +0530 Subject: [PATCH 3/7] Update E2E tests. --- tests/cypress/e2e/block.test.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/cypress/e2e/block.test.js b/tests/cypress/e2e/block.test.js index a492046b..f4d3857d 100644 --- a/tests/cypress/e2e/block.test.js +++ b/tests/cypress/e2e/block.test.js @@ -16,7 +16,13 @@ describe('Block Tests', () => { it('Admin can create a Signup form using Mailchimp block', () => { const postTitle = 'Mailchimp signup form - Block'; const beforeSave = () => { - cy.insertBlock('mailchimp/mailchimp', 'Mailchimp List Subscribe Form'); + cy.insertBlock('mailchimp/mailchimp', 'Mailchimp List Subscribe Form').then( + (blockId) => { + cy.getBlockEditor() + .find(`#${blockId} .block-editor-block-variation-picker__skip button`) + .click(); + }, + ); cy.wait(500); }; cy.createPost({ title: postTitle, content: '', beforeSave }).then((postBlock) => { @@ -239,6 +245,11 @@ describe('Block Tests', () => { cy.get('#mc_signup_submit').should('exist'); cy.visit(`/wp-admin/post.php?post=${oldBlockPostId}&action=edit`); + cy.getBlockEditor() + .find( + '.wp-block-mailchimp-mailchimp .block-editor-block-variation-picker__skip button', + ) + .click(); const header = '[NEW BLOCK] Subscribe to our newsletter'; cy.getBlockEditor() .find('h2[aria-label="Enter a header (optional)"]') From b799d4d653d3269a9a0e5f989f576528c8df9a46 Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Thu, 17 Jul 2025 19:38:58 +0530 Subject: [PATCH 4/7] E2E: Add tests for the form templates --- tests/cypress/config.js | 1 + tests/cypress/e2e/form-templates.test.js | 133 +++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 tests/cypress/e2e/form-templates.test.js diff --git a/tests/cypress/config.js b/tests/cypress/config.js index ffcfddcb..bc4b5d0e 100644 --- a/tests/cypress/config.js +++ b/tests/cypress/config.js @@ -31,6 +31,7 @@ module.exports = defineConfig({ 'tests/cypress/e2e/submission/**.test.js', 'tests/cypress/e2e/validation/**.test.js', 'tests/cypress/e2e/block.test.js', + 'tests/cypress/e2e/form-templates.test.js', 'tests/cypress/e2e/user-sync.test.js', 'tests/cypress/e2e/logout.test.js', ], diff --git a/tests/cypress/e2e/form-templates.test.js b/tests/cypress/e2e/form-templates.test.js new file mode 100644 index 00000000..589d7ac9 --- /dev/null +++ b/tests/cypress/e2e/form-templates.test.js @@ -0,0 +1,133 @@ +/* eslint-disable no-undef */ +describe('Form Templates Tests', () => { + before(() => { + cy.login(); + cy.mailchimpLoginIfNotAlreadyLoggedIn(); + cy.toggleMergeFields('check'); + + // Hide all interest groups + cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options'); + cy.get('input[id^="mc_show_interest_groups_"]').uncheck(); + cy.get('input[value="Update Subscribe Form Settings"]').first().click(); + }); + + it('Admin should see the form templates in the block', () => { + const postTitle = 'Mailchimp signup form - Form Templates'; + const beforeSave = () => { + cy.insertBlock('mailchimp/mailchimp', 'Mailchimp List Subscribe Form').then( + (blockId) => { + cy.getBlockEditor() + .find(`#${blockId} ul.block-editor-block-variation-picker__variations li`) + .should('have.length', 4); + cy.getBlockEditor() + .find( + `#${blockId} ul li:nth-child(1) span.block-editor-block-variation-picker__variation-label`, + ) + .should('have.text', 'Quick Signup (Email Only)'); + cy.getBlockEditor() + .find( + `#${blockId} ul li:nth-child(2) span.block-editor-block-variation-picker__variation-label`, + ) + .should('have.text', 'Personal Signup (Name and Email)'); + cy.getBlockEditor() + .find( + `#${blockId} ul li:nth-child(3) span.block-editor-block-variation-picker__variation-label`, + ) + .should('have.text', 'Contact Form (Contact Details)'); + cy.getBlockEditor() + .find( + `#${blockId} ul li:nth-child(4) span.block-editor-block-variation-picker__variation-label`, + ) + .should('have.text', 'Default Form (All Fields)'); + }, + ); + cy.wait(500); + }; + cy.createPost({ title: postTitle, content: '', beforeSave }); + }); + + const beforeSave = (variation) => { + cy.insertBlock('mailchimp/mailchimp', 'Mailchimp List Subscribe Form').then((blockId) => { + cy.getBlockEditor() + .find( + `#${blockId} ul.block-editor-block-variation-picker__variations li:nth-child(${variation})`, + ) + .click(); + cy.getBlockEditor() + .find('h2[aria-label="Enter a header (optional)"]') + .should('be.visible'); + }); + cy.wait(500); + }; + + it('Admin can select a form template (Quick Signup)', () => { + const postTitle = 'Mailchimp signup form - Form Template 1'; + + cy.createPost({ title: postTitle, content: '', beforeSave: () => beforeSave(1) }).then( + (postBlock) => { + if (postBlock) { + cy.visit(`/?p=${postBlock.id}`); + cy.get('#mc_mv_EMAIL').should('exist'); + cy.get('#mc_signup_submit').should('exist'); + cy.get('#mc_mv_FNAME').should('not.exist'); + cy.get('#mc_mv_LNAME').should('not.exist'); + cy.get('#mc_mv_PHONE').should('not.exist'); + } + }, + ); + }); + + it('Admin can select a form template (Personal Signup)', () => { + const postTitle = 'Mailchimp signup form - Form Template 2'; + cy.createPost({ title: postTitle, content: '', beforeSave: () => beforeSave(2) }).then( + (postBlock2) => { + if (postBlock2) { + cy.visit(`/?p=${postBlock2.id}`); + cy.get('#mc_mv_EMAIL').should('exist'); + cy.get('#mc_signup_submit').should('exist'); + cy.get('#mc_mv_FNAME').should('exist'); + cy.get('#mc_mv_LNAME').should('exist'); + cy.get('input[id^="mc_mv_PHONE"]').should('not.exist'); + } + }, + ); + }); + + it('Admin can select a form template (Contact Form)', () => { + const postTitle = 'Mailchimp signup form - Form Template 3'; + cy.createPost({ title: postTitle, content: '', beforeSave: () => beforeSave(3) }).then( + (postBlock3) => { + if (postBlock3) { + cy.visit(`/?p=${postBlock3.id}`); + cy.get('#mc_mv_EMAIL').should('exist'); + cy.get('#mc_signup_submit').should('exist'); + cy.get('#mc_mv_FNAME').should('exist'); + cy.get('#mc_mv_LNAME').should('exist'); + cy.get('input[id^="mc_mv_PHONE"]').should('exist'); + cy.get('#mc_mv_ADDRESS-addr1').should('exist'); + cy.get('#mc_mv_ADDRESS-addr2').should('exist'); + cy.get('#mc_mv_ADDRESS-city').should('exist'); + } + }, + ); + }); + + it('Admin can select a form template (Default Form)', () => { + const postTitle = 'Mailchimp signup form - Form Template 4'; + cy.createPost({ title: postTitle, content: '', beforeSave: () => beforeSave(4) }).then( + (postBlock4) => { + if (postBlock4) { + cy.visit(`/?p=${postBlock4.id}`); + cy.get('#mc_mv_EMAIL').should('exist'); + cy.get('#mc_signup_submit').should('exist'); + cy.get('#mc_mv_FNAME').should('exist'); + cy.get('#mc_mv_LNAME').should('exist'); + cy.get('input[id^="mc_mv_PHONE"]').should('exist'); + cy.get('#mc_mv_COMPANY').should('exist'); + cy.get('#mc_mv_ADDRESS-addr1').should('exist'); + cy.get('#mc_mv_ADDRESS-addr2').should('exist'); + } + }, + ); + }); +}); From 87b7030494bc7fe985a8cdec547b268e92ce3b40 Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Fri, 18 Jul 2025 12:34:41 +0530 Subject: [PATCH 5/7] Fix display icon in block theme. --- includes/blocks/mailchimp/block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/blocks/mailchimp/block.json b/includes/blocks/mailchimp/block.json index fd6292eb..6f93bc7c 100644 --- a/includes/blocks/mailchimp/block.json +++ b/includes/blocks/mailchimp/block.json @@ -81,5 +81,5 @@ "textdomain": "mailchimp", "editorScript": "file:./index.js", "render": "file:./markup.php", - "editorStyle": "file:./editor.css" + "editorStyle": ["file:./editor.css", "dashicons"] } From 204b86c365601e9440d3e47fce62442401bb8296 Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Wed, 23 Jul 2025 16:36:58 +0530 Subject: [PATCH 6/7] Allow hide required fields in form templates if field is not part of template. --- ...s-mailchimp-list-subscribe-form-blocks.php | 47 ++++++++++ .../blocks/mailchimp-form-field/block.json | 2 +- includes/blocks/mailchimp-form-field/edit.js | 6 +- .../mailchimp-form-field/field-markup.php | 6 ++ includes/blocks/mailchimp/block.json | 7 +- includes/blocks/mailchimp/edit.js | 16 ++-- includes/blocks/mailchimp/markup.php | 12 ++- includes/blocks/mailchimp/variation-picker.js | 28 ++++++ includes/blocks/mailchimp/variations.js | 86 ++++++++++--------- includes/class-mailchimp-form-submission.php | 63 ++++++++------ 10 files changed, 193 insertions(+), 80 deletions(-) diff --git a/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php b/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php index 1e63983b..945863fd 100644 --- a/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php +++ b/includes/blocks/class-mailchimp-list-subscribe-form-blocks.php @@ -211,4 +211,51 @@ public function get_list_data( $request ) { public function get_list_data_permissions_check() { return current_user_can( 'edit_posts' ); } + + /** + * Check if the merge validation should be skipped. + * + * @param array $inner_blocks The inner blocks of the block. + * @param array $merge_fields The merge fields. + * @param string $template The template of the block. + * @return bool True if the merge validation should be skipped, false otherwise. + */ + public function should_skip_merge_validation( $inner_blocks = array(), $merge_fields = array(), $template = 'default' ) { + if ( 'default' === $template ) { + return false; + } + + // Get the tags of the visible inner blocks. + $visible_inner_blocks = array_map( + function ( $block ) { + return $block['attrs']['tag'] ?? ''; + }, + array_filter( + $inner_blocks, + function ( $block ) { + return 'mailchimp/mailchimp-form-field' === $block['blockName'] && isset( $block['attrs']['visible'] ) && $block['attrs']['visible']; + } + ) + ); + + // Get the tags of the required merge fields. + $required_merge_fields = array_map( + function ( $field ) { + return $field['tag'] ?? ''; + }, + array_filter( + $merge_fields, + function ( $field ) { + return $field['required']; + } + ) + ); + + $missing_required_fields = array_diff( $required_merge_fields, $visible_inner_blocks ); + if ( ! empty( $missing_required_fields ) ) { + return true; + } + + return false; + } } diff --git a/includes/blocks/mailchimp-form-field/block.json b/includes/blocks/mailchimp-form-field/block.json index 7707dd8c..7f5e29ff 100644 --- a/includes/blocks/mailchimp-form-field/block.json +++ b/includes/blocks/mailchimp-form-field/block.json @@ -30,7 +30,7 @@ "parent": [ "mailchimp/mailchimp" ], - "usesContext": ["mailchimp/list_id","mailchimp/show_required_indicator"], + "usesContext": ["mailchimp/list_id","mailchimp/show_required_indicator","mailchimp/template"], "editorScript": "file:./index.js", "render": "file:./field-markup.php" } diff --git a/includes/blocks/mailchimp-form-field/edit.js b/includes/blocks/mailchimp-form-field/edit.js index 115eb274..b1dcd9d4 100644 --- a/includes/blocks/mailchimp-form-field/edit.js +++ b/includes/blocks/mailchimp-form-field/edit.js @@ -312,11 +312,13 @@ export const BlockEdit = (props) => { const { attributes, setAttributes, - context: { 'mailchimp/list_id': listId }, + context: { 'mailchimp/list_id': listId, 'mailchimp/template': template }, } = props; const { visible, tag } = attributes; const { mailchimpListData } = window; - const isRequired = mailchimpListData?.[listId]?.mergeFields?.[tag]?.required || false; + const isRequired = + (template === 'default' && mailchimpListData?.[listId]?.mergeFields?.[tag]?.required) || + tag === 'EMAIL'; return (
diff --git a/includes/blocks/mailchimp-form-field/field-markup.php b/includes/blocks/mailchimp-form-field/field-markup.php index 83e589ae..d622ee69 100644 --- a/includes/blocks/mailchimp-form-field/field-markup.php +++ b/includes/blocks/mailchimp-form-field/field-markup.php @@ -7,6 +7,7 @@ $list_id = $block->context['mailchimp/list_id'] ?? ''; $show_required_indicator = $block->context['mailchimp/show_required_indicator'] ?? true; +$template = $block->context['mailchimp/template'] ?? 'default'; $field_tag = $attributes['tag'] ?? ''; $label = $attributes['label'] ?? ''; $is_visible = $attributes['visible'] ?? false; @@ -32,6 +33,11 @@ function ( $field ) use ( $field_tag ) { return; } +// If the template is not default and the field is marked as hidden, don't render the field. +if ( 'default' !== $template && ! $is_visible && 'EMAIL' !== $field_tag ) { + return; +} + ?>
> { unsubscribe_link_text, show_required_indicator = true, required_indicator_text, + template = 'default', } = attributes; const [listData, setListData] = useState({}); @@ -122,16 +123,17 @@ export const BlockEdit = (props) => { type: field.type, /** * Visibility logic: - * 1. If there are visible fields from the previous list, make the field visible if it's required or it's visible in the previous list form (Try to keep the same visibility as the previous list form). + * 1. If there are visible fields from the previous list, make the field visible if it's visible in the previous list form (Try to keep the same visibility as the previous list form) for the default template also make the field visible if it's required. * 2. If there are no visible fields from the previous list, make the field visible if it's required or it's public and the visibility setting is on in the global settings. */ visible: - field.required || + (template === 'default' && field.required) || (visibleFields.length > 0 && visibleFields.includes(field.tag)) || (visibleFields.length === 0 && - merge_fields_visibility?.[field.tag] === 'on' && - field.public), + (field.required || + (merge_fields_visibility?.[field.tag] === 'on' && + field.public))), }), ) || []; const listGroupsBlocks = @@ -171,7 +173,7 @@ export const BlockEdit = (props) => { tag: field.tag, label: field.name, type: field.type, - visible: field.required, // Keep newly added fields hidden by default, except for required fields. + visible: template === 'default' && field.required, // Keep newly added fields hidden by default, except for required fields. }), ); const newGroupBlocks = newFormGroups.map((group) => @@ -311,7 +313,7 @@ export const BlockEdit = (props) => { visible: interest_groups_visibility?.[group.id] === 'on' && group.type !== 'hidden', }, ]) || []; - const template = [...templateFields, ...templateGroups]; + const templateBlocks = [...templateFields, ...templateGroups]; return ( <> @@ -353,7 +355,7 @@ export const BlockEdit = (props) => { {show_required_indicator && ( diff --git a/includes/blocks/mailchimp/markup.php b/includes/blocks/mailchimp/markup.php index 843e5da1..79bd4fcc 100644 --- a/includes/blocks/mailchimp/markup.php +++ b/includes/blocks/mailchimp/markup.php @@ -42,19 +42,22 @@ function ( $single_list ) { $header = $attributes['header'] ?? ''; $sub_heading = $attributes['sub_header'] ?? ''; $submit_text = $attributes['submit_text'] ?? __( 'Subscribe', 'mailchimp' ); - $merge_fields = get_option( 'mailchimp_sf_merge_fields_' . $list_id ); + $merge_fields = get_option( 'mailchimp_sf_merge_fields_' . $list_id, array() ); $show_unsubscribe_link = $attributes['show_unsubscribe_link'] ?? get_option( 'mc_use_unsub_link' ) === 'on'; $unsubscribe_link_text = $attributes['unsubscribe_link_text'] ?? __( 'unsubscribe from list', 'mailchimp' ); $update_existing_subscribers = ( $attributes['update_existing_subscribers'] ?? get_option( 'mc_update_existing' ) === 'on' ) ? 'yes' : 'no'; $double_opt_in = ( $attributes['double_opt_in'] ?? get_option( 'mc_double_optin' ) === 'on' ) ? 'yes' : 'no'; $show_required_indicator = $attributes['show_required_indicator'] ?? true; $required_indicator_text = $attributes['required_indicator_text'] ?? __( '* = required field', 'mailchimp' ); + $template = $attributes['template'] ?? 'default'; + $skip_merge_validation = ( new Mailchimp_List_Subscribe_Form_Blocks() )->should_skip_merge_validation( $inner_blocks, $merge_fields, $template ) ? 'yes' : 'no'; $hash = wp_hash( serialize( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize array( - 'list_id' => $list_id, - 'update_existing' => $update_existing_subscribers, - 'double_opt_in' => $double_opt_in, + 'list_id' => $list_id, + 'update_existing' => $update_existing_subscribers, + 'double_opt_in' => $double_opt_in, + 'skip_merge_validation' => $skip_merge_validation, ) ) ); @@ -108,6 +111,7 @@ function ( $single_list ) { + { const variationName = variation.name; const variationFields = formFields[variationName] || []; @@ -22,6 +24,17 @@ const getMissingFields = (variation) => { return variationFields.filter((field) => !formFieldsTags.includes(field)); }; +const getHiddenRequiredFields = (variation) => { + const variationName = variation.name; + if (variationName === 'default') { + return []; + } + + const requiredFields = merge_fields.filter((field) => field.required).map((field) => field.tag); + const variationFields = formFields[variationName] || []; + return requiredFields.filter((field) => !variationFields.includes(field)); +}; + export const VariationPicker = ({ name, setAttributes, clientId }) => { const { blockType, defaultVariation, variations } = useSelect( (select) => { @@ -54,12 +67,27 @@ export const VariationPicker = ({ name, setAttributes, clientId }) => { if (nextVariation.innerBlocks) { const missingFields = getMissingFields(nextVariation); + const hiddenRequiredFields = getHiddenRequiredFields(nextVariation); replaceInnerBlocks( clientId, createBlocksFromInnerBlocksTemplate(nextVariation.innerBlocks), ); + // Add a notice if there are required fields missing from the selected form template. + if (hiddenRequiredFields.length > 0) { + createNotice( + 'warning', + sprintf( + __( + 'The selected form template is missing some required fields (%s) for Mailchimp, so merge validation will be turned off for this form.', + 'mailchimp', + ), + hiddenRequiredFields.join(', '), + ), + ); + } + // Add a notice if there are missing fields from the selected form template. if (missingFields.length > 0) { createNotice( diff --git a/includes/blocks/mailchimp/variations.js b/includes/blocks/mailchimp/variations.js index 657d4038..e9132ea0 100644 --- a/includes/blocks/mailchimp/variations.js +++ b/includes/blocks/mailchimp/variations.js @@ -8,21 +8,29 @@ const { merge_fields = [], interest_groups = [], merge_fields_visibility = {}, + interest_groups_visibility = {}, } = mailchimp_sf_block_data; /** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */ -const groupInnerBlocks = interest_groups.map((group) => [ - 'mailchimp/mailchimp-audience-group', - { - id: group.id, - label: group.title, - visible: false, - }, -]); +export const formFields = { + 'email-only-form': ['EMAIL'], + 'name-and-email-form': ['FNAME', 'LNAME', 'EMAIL'], + 'contact-form': ['FNAME', 'LNAME', 'EMAIL', 'PHONE', 'ADDRESS'], + default: [], +}; + +export const formFieldTitles = { + FNAME: __('First Name', 'mailchimp'), + LNAME: __('Last Name', 'mailchimp'), + EMAIL: __('Email', 'mailchimp'), + PHONE: __('Phone', 'mailchimp'), + ADDRESS: __('Address', 'mailchimp'), +}; -const prepareInnerBlocks = (merge_fields = [], fields = []) => { - return [...merge_fields] +const prepareInnerBlocks = (merge_fields = [], template = 'default') => { + const fields = formFields[template] || []; + const fieldInnerBlocks = [...merge_fields] .sort((a, b) => { const aIndex = fields.indexOf(a.tag); const bIndex = fields.indexOf(b.tag); @@ -30,7 +38,7 @@ const prepareInnerBlocks = (merge_fields = [], fields = []) => { }) .map((field) => { let visible = - field.required || + (template === 'default' && field.required) || fields.includes(field.tag) || fields.includes(field.type?.toUpperCase()); if (fields.length === 0) { @@ -46,21 +54,18 @@ const prepareInnerBlocks = (merge_fields = [], fields = []) => { }, ]; }); -}; - -export const formFields = { - 'email-only-form': ['EMAIL'], - 'name-and-email-form': ['FNAME', 'LNAME', 'EMAIL'], - 'contact-form': ['FNAME', 'LNAME', 'EMAIL', 'PHONE', 'ADDRESS'], - default: [], -}; - -export const formFieldTitles = { - FNAME: __('First Name', 'mailchimp'), - LNAME: __('Last Name', 'mailchimp'), - EMAIL: __('Email', 'mailchimp'), - PHONE: __('Phone', 'mailchimp'), - ADDRESS: __('Address', 'mailchimp'), + const groupInnerBlocks = interest_groups.map((group) => [ + 'mailchimp/mailchimp-audience-group', + { + id: group.id, + label: group.title, + visible: + template !== 'default' + ? false + : interest_groups_visibility?.[group.id] === 'on' && group.type !== 'hidden', + }, + ]); + return [...fieldInnerBlocks, ...groupInnerBlocks]; }; /** @@ -74,10 +79,10 @@ export const variations = [ title: __('Quick Signup (Email Only)', 'mailchimp'), description: __('A quick signup form with only an email field.', 'mailchimp'), icon: 'email', - innerBlocks: [ - ...prepareInnerBlocks(merge_fields, formFields['email-only-form']), - ...groupInnerBlocks, - ], + attributes: { + template: 'email-only-form', + }, + innerBlocks: prepareInnerBlocks(merge_fields, 'email-only-form'), scope: ['block'], }, { @@ -85,10 +90,10 @@ export const variations = [ title: __('Personal Signup (Name and Email)', 'mailchimp'), description: __('A personal signup form with only a name and email fields', 'mailchimp'), icon: 'admin-users', - innerBlocks: [ - ...prepareInnerBlocks(merge_fields, formFields['name-and-email-form']), - ...groupInnerBlocks, - ], + attributes: { + template: 'name-and-email-form', + }, + innerBlocks: prepareInnerBlocks(merge_fields, 'name-and-email-form'), scope: ['block'], }, { @@ -99,10 +104,10 @@ export const variations = [ 'mailchimp', ), icon: 'id', - innerBlocks: [ - ...prepareInnerBlocks(merge_fields, formFields['contact-form']), - ...groupInnerBlocks, - ], + attributes: { + template: 'contact-form', + }, + innerBlocks: prepareInnerBlocks(merge_fields, 'contact-form'), scope: ['block'], }, { @@ -110,8 +115,11 @@ export const variations = [ title: __('Default Form (All Fields)', 'mailchimp'), description: __('A default form, Fields based on settings.', 'mailchimp'), icon: 'admin-settings', + attributes: { + template: 'default', + }, isDefault: true, - innerBlocks: [...prepareInnerBlocks(merge_fields, formFields.default), ...groupInnerBlocks], + innerBlocks: prepareInnerBlocks(merge_fields, 'default'), scope: ['block'], }, ]; diff --git a/includes/class-mailchimp-form-submission.php b/includes/class-mailchimp-form-submission.php index 96fd7078..f111c8b6 100644 --- a/includes/class-mailchimp-form-submission.php +++ b/includes/class-mailchimp-form-submission.php @@ -92,24 +92,27 @@ public function handle_form_submission() { return new WP_Error( 'mailchimp-invalid-form', esc_html__( 'Invalid form submission.', 'mailchimp' ) ); } - $list_id = get_option( 'mc_list_id' ); - $update_existing = get_option( 'mc_update_existing' ); - $double_opt_in = get_option( 'mc_double_optin' ); - $merge_fields = get_option( 'mc_merge_vars', array() ); - $interest_groups = get_option( 'mc_interest_groups', array() ); + $list_id = get_option( 'mc_list_id' ); + $update_existing = get_option( 'mc_update_existing' ); + $double_opt_in = get_option( 'mc_double_optin' ); + $skip_merge_validation = false; + $merge_fields = get_option( 'mc_merge_vars', array() ); + $interest_groups = get_option( 'mc_interest_groups', array() ); // Check if request from latest block. if ( isset( $_POST['mailchimp_sf_list_id'] ) ) { - $list_id = isset( $_POST['mailchimp_sf_list_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_list_id'] ) ) : ''; - $update_existing = isset( $_POST['mailchimp_sf_update_existing_subscribers'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_update_existing_subscribers'] ) ) : ''; - $double_opt_in = isset( $_POST['mailchimp_sf_double_opt_in'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_double_opt_in'] ) ) : ''; - $hash = isset( $_POST['mailchimp_sf_hash'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_hash'] ) ) : ''; - $expected = wp_hash( + $list_id = isset( $_POST['mailchimp_sf_list_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_list_id'] ) ) : ''; + $update_existing = isset( $_POST['mailchimp_sf_update_existing_subscribers'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_update_existing_subscribers'] ) ) : ''; + $double_opt_in = isset( $_POST['mailchimp_sf_double_opt_in'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_double_opt_in'] ) ) : ''; + $skip_merge_validation = isset( $_POST['mailchimp_sf_skip_merge_validation'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_skip_merge_validation'] ) ) : ''; + $hash = isset( $_POST['mailchimp_sf_hash'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_hash'] ) ) : ''; + $expected = wp_hash( serialize( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize array( - 'list_id' => $list_id, - 'update_existing' => $update_existing, - 'double_opt_in' => $double_opt_in, + 'list_id' => $list_id, + 'update_existing' => $update_existing, + 'double_opt_in' => $double_opt_in, + 'skip_merge_validation' => $skip_merge_validation, ) ) ); @@ -119,15 +122,16 @@ public function handle_form_submission() { return new WP_Error( 'mailchimp-invalid-hash', esc_html__( 'Invalid form submission.', 'mailchimp' ) ); } - $update_existing = 'yes' === $update_existing; - $double_opt_in = 'yes' === $double_opt_in; - $merge_fields = get_option( 'mailchimp_sf_merge_fields_' . $list_id, array() ); - $interest_groups = get_option( 'mailchimp_sf_interest_groups_' . $list_id, array() ); + $update_existing = 'yes' === $update_existing; + $double_opt_in = 'yes' === $double_opt_in; + $skip_merge_validation = 'yes' === $skip_merge_validation; + $merge_fields = get_option( 'mailchimp_sf_merge_fields_' . $list_id, array() ); + $interest_groups = get_option( 'mailchimp_sf_interest_groups_' . $list_id, array() ); } // Prepare request body $email = isset( $_POST['mc_mv_EMAIL'] ) ? wp_strip_all_tags( wp_unslash( $_POST['mc_mv_EMAIL'] ) ) : ''; - $merge_fields_body = $this->prepare_merge_fields_body( $merge_fields ); + $merge_fields_body = $this->prepare_merge_fields_body( $merge_fields, $skip_merge_validation ); // Catch errors and fail early. if ( is_wp_error( $merge_fields_body ) ) { @@ -149,11 +153,12 @@ public function handle_form_submission() { $list_id, $email, array( - 'email_type' => $email_type, - 'merge_fields' => $merge_fields_body, - 'interests' => $groups, - 'update_existing' => $update_existing, - 'double_opt_in' => $double_opt_in, + 'email_type' => $email_type, + 'merge_fields' => $merge_fields_body, + 'interests' => $groups, + 'update_existing' => $update_existing, + 'double_opt_in' => $double_opt_in, + 'skip_merge_validation' => $skip_merge_validation, ) ); @@ -176,10 +181,11 @@ public function handle_form_submission() { /** * Prepare the merge fields body for the API request. * - * @param array $merge_fields Merge fields. + * @param array $merge_fields Merge fields. + * @param bool $skip_merge_validation Skip merge validation. * @return stdClass|WP_Error */ - public function prepare_merge_fields_body( $merge_fields ) { + public function prepare_merge_fields_body( $merge_fields, $skip_merge_validation = false ) { // Loop through our merge fields, and if they're empty, but required, then print an error, and mark as failed $merge = new stdClass(); foreach ( $merge_fields as $merge_field ) { @@ -187,7 +193,7 @@ public function prepare_merge_fields_body( $merge_fields ) { $opt = 'mc_mv_' . $tag; // Skip if the field is not required and not submitted. - if ( 'Y' !== $merge_field['required'] && ! isset( $_POST[ $opt ] ) ) { + if ( ( 'Y' !== $merge_field['required'] && ! isset( $_POST[ $opt ] ) ) || $skip_merge_validation ) { continue; } @@ -414,6 +420,11 @@ protected function subscribe_to_list( $list_id, $email, $args ) { return new WP_Error( 'mailchimp-update-existing', $msg ); } + // Add skip merge validation for handle hidden required fields for the form template. + if ( isset( $args['skip_merge_validation'] ) && $args['skip_merge_validation'] ) { + $url .= '?skip_merge_validation=true'; + } + // Prepare request body $request_body = $this->prepare_subscribe_request_body( $email, $status, $args ); $response = $api->post( $url, $request_body, 'PUT', $list_id ); From 31ccd7186c13e641ee0b8b441484709e9a728bee Mon Sep 17 00:00:00 2001 From: Dharmesh Patel Date: Fri, 1 Aug 2025 19:57:46 +0530 Subject: [PATCH 7/7] Display user-friendly name in notice. --- includes/blocks/mailchimp/variation-picker.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/includes/blocks/mailchimp/variation-picker.js b/includes/blocks/mailchimp/variation-picker.js index 3d4d222b..a009cde6 100644 --- a/includes/blocks/mailchimp/variation-picker.js +++ b/includes/blocks/mailchimp/variation-picker.js @@ -32,7 +32,9 @@ const getHiddenRequiredFields = (variation) => { const requiredFields = merge_fields.filter((field) => field.required).map((field) => field.tag); const variationFields = formFields[variationName] || []; - return requiredFields.filter((field) => !variationFields.includes(field)); + return requiredFields + .filter((field) => !variationFields.includes(field)) + .map((field) => formFieldTitles[field] || field); }; export const VariationPicker = ({ name, setAttributes, clientId }) => { @@ -50,7 +52,7 @@ export const VariationPicker = ({ name, setAttributes, clientId }) => { [name], ); const { replaceInnerBlocks } = useDispatch(blockEditorStore); - const { createNotice } = useDispatch('core/notices'); + const { createNotice, removeNotice } = useDispatch('core/notices'); const blockProps = useBlockProps(); return ( @@ -69,6 +71,10 @@ export const VariationPicker = ({ name, setAttributes, clientId }) => { const missingFields = getMissingFields(nextVariation); const hiddenRequiredFields = getHiddenRequiredFields(nextVariation); + // Remove any existing notices. + removeNotice('mailchimp-form-template-required-field-notice'); + removeNotice('mailchimp-form-template-field-notice'); + replaceInnerBlocks( clientId, createBlocksFromInnerBlocksTemplate(nextVariation.innerBlocks), @@ -85,6 +91,10 @@ export const VariationPicker = ({ name, setAttributes, clientId }) => { ), hiddenRequiredFields.join(', '), ), + { + id: 'mailchimp-form-template-required-field-notice', + isDismissible: true, + }, ); }