feat: explain the type collision on contact fields (trial)

A contact-field item whose type is no allowed child now gets a hint that
type is the node type, with the attrs path (fieldItem.advanced.type) and
the lit-divi-update set alternative, instead of "unbekanntes Modul
divi/email". The metadata fixture gains contact-form/contact-field
(regenerated from Divi 5.2.1, additions only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 19:22:13 +02:00
co-authored by Claude Opus 5.5
parent 26108bc9a9
commit cda8b025cc
4 changed files with 975 additions and 2 deletions
+37 -1
View File
@@ -256,7 +256,7 @@ final class Composer
$violation = $this->rules->violation($parent, $type); $violation = $this->rules->violation($parent, $type);
if ($violation !== null) { if ($violation !== null) {
$this->errors[$path . '.type'] = $violation; $this->errors[$path . '.type'] = $this->typeFieldCollision($spec, $parent) ?? $violation;
return null; return null;
} }
@@ -268,6 +268,42 @@ final class Composer
}; };
} }
/**
* Trial feedback: an item whose only possible type has a short-form field named "type" (the
* contact field's field type) – a `type` that is no allowed child was almost certainly meant as
* that field. Explains the collision instead of "unbekanntes Modul divi/<value>".
*
* @param array<array-key, mixed> $spec
*/
private function typeFieldCollision(array $spec, ?Block $parent): ?string
{
$children = $parent === null ? [] : $this->catalog->childrenOf($parent->name());
if (count($children) !== 1 || ! is_string($spec['type'] ?? null)) {
return null;
}
$child = ModuleCatalog::shortName($children[0]);
$field = ShortformMap::fields($child)['type'] ?? null;
if ($field === null) {
return null;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure PHP (spec §4.4).
$value = json_encode(
$spec['type'],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
);
return sprintf(
'`type` ist hier der Knotentyp (%1$s, darf fehlen); das Feldtyp-Feld über attrs setzen: '
. '{"attrs": {"%2$s": {"desktop": %3$s}}} – oder type weglassen und danach mit lit-divi-update '
. '{"set": {"type": %3$s}} setzen.',
$child,
(string) $field['path'],
(string) $value
);
}
/** @param array<array-key, mixed> $spec */ /** @param array<array-key, mixed> $spec */
private function typeOf(array $spec, ?Block $parent): ?string private function typeOf(array $spec, ?Block $parent): ?string
{ {
+915
View File
@@ -3530,6 +3530,821 @@
} }
} }
} }
},
{
"name": "divi/contact-form",
"title": "Contact Form",
"category": "module",
"childrenName": [
"divi/contact-field"
],
"childModuleName": "divi/contact-field",
"attributes": {
"module": {
"settings": {
"meta": {
"meta": []
},
"advanced": {
"elements": {
"groupType": "group-item",
"item": {
"groupSlug": "contentElements",
"priority": 5,
"render": true,
"component": {
"type": "group",
"name": "divi/elements",
"props": {
"grouped": false
}
}
}
},
"html": [],
"link": [],
"loop": [],
"text": {
"component": {
"props": {
"fields": {
"color": {
"render": false
}
}
}
}
},
"successMessage": {
"groupType": "group-item",
"item": {
"groupSlug": "contentText",
"priority": 10,
"render": true,
"attrName": "module.advanced.successMessage",
"label": "Success Message",
"description": "Type the message you want to display after successful form submission. Leave blank for default",
"category": "configuration",
"features": {
"dynamicContent": {
"type": "text"
},
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/text",
"type": "field"
}
}
}
},
"decoration": {
"layout": [],
"animation": [],
"attributes": [],
"background": [],
"border": {
"component": {
"props": {
"fieldLabel": "Inputs"
}
}
},
"boxShadow": [],
"conditions": [],
"disabledOn": [],
"filters": [],
"interactions": [],
"overflow": [],
"order": [],
"position": [],
"scroll": [],
"sizing": [],
"spacing": [],
"sticky": [],
"transform": [],
"transition": [],
"zIndex": []
}
}
},
"title": {
"settings": {
"innerContent": {
"groupType": "group-item",
"item": {
"groupSlug": "contentText",
"priority": 5,
"render": true,
"attrName": "title.innerContent",
"label": "Title",
"description": "Define a title for your contact form.",
"category": "basic_option",
"features": {
"dynamicContent": {
"type": "text"
},
"sticky": false,
"preset": "content"
},
"component": {
"name": "divi/text",
"type": "field"
}
}
},
"decoration": {
"font": {
"priority": 10,
"component": {
"props": {
"groupLabel": "Title Text",
"fieldLabel": "Title",
"dynamicSubgroupHost": true,
"dynamicSubgroupHostLayoutStyle": "moduleLayout",
"fields": {
"headingLevel": {
"render": true
}
}
}
}
}
}
}
},
"captcha": {
"settings": {
"decoration": {
"font": {
"priority": 15,
"component": {
"props": {
"groupLabel": "Captcha Text",
"fieldLabel": "Captcha",
"dynamicSubgroupHost": true,
"fields": {
"textAlign": {
"render": false
}
}
}
}
}
}
}
},
"field": {
"settings": {
"decoration": {
"background": [],
"font": [],
"spacing": []
},
"advanced": {
"focus": []
}
}
},
"button": {
"settings": {
"innerContent": {
"groupType": "group-item",
"item": {
"groupSlug": "contentText",
"priority": 20,
"render": true,
"attrName": "button.innerContent",
"subName": "text",
"label": "Submit Button",
"description": "Define the text of the form submit button.",
"category": "basic_option",
"features": {
"dynamicContent": {
"type": "text"
},
"sticky": false,
"preset": "content"
},
"component": {
"name": "divi/text",
"type": "field"
}
}
},
"decoration": {
"background": [],
"border": [],
"boxShadow": [],
"button": {
"component": {
"props": {
"dynamicSubgroupHost": true
}
}
},
"font": [],
"sizing": [],
"spacing": []
}
}
},
"email": {
"settings": {
"advanced": {
"receiver": {
"groupType": "group-item",
"item": {
"groupSlug": "contentEmail",
"priority": 5,
"render": true,
"attrName": "email.advanced.receiver",
"label": "Email Address",
"description": "Input the email address where messages should be sent.<br /><br /> Note: email delivery and spam prevention are complex processes. We recommend using a delivery service such as <a href=\"http://mandrill.com/\">Mandrill</a>, <a href=\"https://sendgrid.com/\">SendGrid</a>, or other similar service to ensure the deliverability of messages that are submitted through this form",
"category": "basic_option",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content",
"dynamicContent": {
"type": "text"
}
},
"component": {
"name": "divi/text",
"type": "field"
}
}
}
},
"innerContent": {
"groupType": "group-item",
"item": {
"groupSlug": "contentEmail",
"priority": 10,
"render": true,
"attrName": "email.innerContent",
"label": "Message Pattern",
"description": "Here you can define the custom pattern for the email Message. Fields should be included in following format - <strong>%%field_id%%</strong>. For example if you want to include the field with id = <strong>phone</strong> and field with id = <strong>message</strong>, then you can use the following pattern: <strong>My message is %%message%% and phone number is %%phone%%</strong>. Leave blank for default.",
"category": "configuration",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content",
"dynamicContent": {
"type": "text"
}
},
"component": {
"name": "divi/textarea",
"type": "field",
"props": {
"processing": "custom_message"
}
}
}
}
}
},
"redirect": {
"settings": {
"advanced": {
"useRedirect": {
"groupType": "group-item",
"item": {
"groupSlug": "contentRedirect",
"priority": 5,
"render": true,
"attrName": "redirect.advanced.useRedirect",
"label": "Enable Redirect URL",
"description": "Redirect users after successful form submission.",
"category": "configuration",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content"
},
"component": {
"name": "divi/toggle",
"type": "field"
}
}
}
},
"innerContent": {
"groupType": "group-item",
"item": {
"groupSlug": "contentRedirect",
"priority": 10,
"render": true,
"attrName": "redirect.innerContent",
"label": "Redirect URL",
"description": "Type the Redirect URL",
"category": "configuration",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content",
"dynamicContent": {
"type": "url"
}
},
"component": {
"name": "divi/text",
"type": "field"
}
}
}
}
}
}
},
{
"name": "divi/contact-field",
"title": "Field",
"category": "child-module",
"childrenName": [],
"attributes": {
"module": {
"settings": {
"meta": {
"meta": []
},
"advanced": {
"elements": [],
"html": [],
"link": [],
"loop": [],
"text": {
"component": {
"props": {
"fields": {
"orientation": {
"component": {
"props": {
"allowClickToUnset": true,
"renderEmptyStringAsDefaultValue": true
}
}
},
"color": {
"render": false
},
"textShadowGroup": {
"render": false
}
}
}
}
}
},
"decoration": {
"layout": [],
"background": [],
"border": {
"component": {
"props": {
"fieldLabel": "Input"
}
}
},
"animation": [],
"attributes": [],
"boxShadow": [],
"conditions": [],
"disabledOn": [],
"filters": [],
"interactions": [],
"overflow": [],
"order": [],
"position": [],
"scroll": [],
"sizing": [],
"spacing": [],
"sticky": [],
"transform": [],
"transition": [],
"zIndex": []
}
}
},
"field": {
"settings": {
"decoration": {
"background": [],
"font": []
},
"advanced": {
"focus": []
}
}
},
"fieldTitle": {
"settings": {
"decoration": {
"font": {
"priority": 10,
"component": {
"props": {
"groupLabel": "Title Text",
"fieldLabel": "Title"
}
}
}
}
}
},
"fieldItem": {
"settings": {
"advanced": {
"id": {
"groupType": "group-item",
"item": {
"groupSlug": "contentText",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.id",
"label": "Field ID",
"description": "Define the unique ID of this field. You should use only English characters without special characters and spaces.",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content",
"dynamicContent": {
"type": "text"
}
},
"component": {
"name": "divi/text",
"type": "field"
}
}
},
"type": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.type",
"label": "Type",
"description": "Choose the type of field",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/select",
"type": "field",
"props": {
"options": {
"input": {
"label": "Input Field"
},
"email": {
"label": "Email Field"
},
"text": {
"label": "Textarea"
},
"checkbox": {
"label": "Checkboxes"
},
"radio": {
"label": "Radio Buttons"
},
"select": {
"label": "Select Dropdown"
}
}
}
}
}
},
"minLength": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.minLength",
"label": "Minimum Length",
"description": "Leave at 0 to remove restriction",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/range",
"type": "field",
"props": {
"allowedUnits": [],
"defaultUnit": ""
}
}
}
},
"maxLength": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.maxLength",
"label": "Maximum Length",
"description": "Leave at 0 to remove restriction",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/range",
"type": "field",
"props": {
"allowedUnits": [],
"defaultUnit": ""
}
}
}
},
"allowedSymbols": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.allowedSymbols",
"label": "Allowed Symbols",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/select",
"type": "field",
"props": {
"options": {
"all": {
"label": "All"
},
"letters": {
"label": "Letters Only (A-Z)"
},
"numbers": {
"label": "Numbers Only (0-9)"
},
"alphanumeric": {
"label": "Alphanumeric Only (A-Z, 0-9)"
}
}
}
}
}
},
"checkboxOptions": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.checkboxOptions",
"label": "Options",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/sortable-list",
"type": "field",
"props": {
"isCheckbox": true,
"addFirstOnMount": true,
"actions": {
"right": [
"move",
"link",
"copy",
"delete"
]
}
}
}
}
},
"radioOptions": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.radioOptions",
"label": "Options",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/sortable-list",
"type": "field",
"props": {
"isRadio": true,
"addFirstOnMount": true,
"actions": {
"right": [
"move",
"link",
"copy",
"delete"
]
}
}
}
}
},
"selectOptions": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.selectOptions",
"label": "Options",
"category": "basic_option",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/sortable-list",
"type": "field",
"props": {
"addFirstOnMount": true,
"actions": {
"right": [
"move",
"copy",
"delete"
]
}
}
}
}
},
"required": {
"groupType": "group-item",
"item": {
"groupSlug": "contentFieldOptions",
"priority": 5,
"render": true,
"attrName": "fieldItem.advanced.required",
"label": "Required Field",
"description": "Define whether the field should be required or optional",
"category": "configuration",
"features": {
"sticky": false,
"responsive": false,
"hover": false,
"preset": "content"
},
"component": {
"name": "divi/toggle",
"type": "field"
}
}
}
},
"innerContent": {
"groupType": "group-item",
"item": {
"groupSlug": "contentText",
"priority": 10,
"render": true,
"attrName": "fieldItem.innerContent",
"label": "Title",
"description": "Here you can define the content that will be placed within the current tab.",
"category": "basic_option",
"features": {
"sticky": false,
"preset": "content",
"dynamicContent": {
"type": "text"
}
},
"component": {
"name": "divi/text",
"type": "field"
}
}
}
}
},
"conditionalLogic": {
"settings": {
"innerContent": {
"groupType": "group-item",
"item": {
"groupSlug": "contentConditionalLogic",
"priority": 10,
"render": true,
"attrName": "conditionalLogic.innerContent",
"label": "Rules",
"category": "layout",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content"
},
"component": {
"name": "divi/conditions",
"type": "field"
}
}
},
"advanced": {
"enable": {
"groupType": "group-item",
"item": {
"groupSlug": "contentConditionalLogic",
"priority": 5,
"render": true,
"attrName": "conditionalLogic.advanced.enable",
"label": "Enable",
"description": "Enabling conditional logic makes this field only visible when any or all of the rules below are fulfilled<br><strong>Note:</strong> Only fields with an unique and non-empty field ID can be used",
"category": "layout",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content"
},
"component": {
"name": "divi/toggle",
"type": "field"
}
}
},
"relation": {
"groupType": "group-item",
"item": {
"groupSlug": "contentConditionalLogic",
"priority": 5,
"render": true,
"attrName": "conditionalLogic.advanced.relation",
"label": "Relation",
"description": "Choose whether any or all of the rules should be fulfilled",
"category": "layout",
"features": {
"responsive": false,
"hover": false,
"sticky": false,
"preset": "content"
},
"component": {
"name": "divi/toggle",
"type": "field",
"props": {
"options": {
"off": "Any",
"on": "All"
}
}
}
}
}
}
}
}
}
} }
], ],
"defaults": { "defaults": {
@@ -3972,6 +4787,106 @@
} }
} }
} }
},
"divi/contact-form": {
"module": {
"meta": {
"adminLabel": {
"desktop": {
"value": "Contact Form"
}
}
},
"advanced": {
"spamProtection": {
"desktop": {
"value": {
"enabled": "off",
"useBasicCaptcha": "on"
}
}
}
}
},
"title": {
"decoration": {
"font": {
"font": {
"desktop": {
"value": {
"headingLevel": "h1"
}
}
}
}
}
},
"button": {
"innerContent": {
"desktop": {
"value": {
"text": "Submit"
}
}
},
"decoration": {
"button": {
"desktop": {
"value": {
"icon": {
"enable": "on"
}
}
}
}
}
},
"redirect": {
"advanced": {
"useRedirect": {
"desktop": {
"value": "off"
}
}
}
}
},
"divi/contact-field": {
"module": {
"decoration": {
"sizing": {
"desktop": {
"value": {
"flexType": "24_24"
}
}
}
}
},
"fieldItem": {
"advanced": {
"id": {
"desktop": {
"value": ""
}
},
"type": {
"desktop": {
"value": "input"
}
},
"required": {
"desktop": {
"value": "on"
}
}
},
"innerContent": {
"desktop": {
"value": "New Field"
}
}
}
} }
} }
} }
+1 -1
View File
@@ -18,7 +18,7 @@ if (! class_exists(ModuleRegistration::class)) {
$wanted = [ $wanted = [
'section', 'row', 'column', 'text', 'heading', 'button', 'image', 'blurb', 'code', 'accordion', 'section', 'row', 'column', 'text', 'heading', 'button', 'image', 'blurb', 'code', 'accordion',
'accordion-item', 'fullwidth-header', 'group', 'accordion-item', 'fullwidth-header', 'group', 'contact-form', 'contact-field',
]; ];
$all = ModuleRegistration::get_all_core_modules_metadata(); $all = ModuleRegistration::get_all_core_modules_metadata();
$metadata = []; $metadata = [];
@@ -530,4 +530,26 @@ final class ComposerTest extends TestCase
self::assertStringContainsString('2 Spalten, aber 3 Modul-Listen', $result['errors']['nodes[0].rows[0].modules']); self::assertStringContainsString('2 Spalten, aber 3 Modul-Listen', $result['errors']['nodes[0].rows[0].modules']);
} }
/**
* Trial feedback: `type` on a contact field is the node type, not the field type. A value that
* is no allowed child gets a dedicated hint instead of "unbekanntes Modul divi/email".
*/
public function testTypeOnAnItemWithATypeFieldExplainsTheCollision(): void
{
$result = ComposeFixture::composer()->compose([['type' => 'section', 'modules' => [
['type' => 'contact-form', 'items' => [
['title' => 'Name'],
['type' => 'email', 'title' => 'E-Mail'],
['type' => 'contact-field', 'title' => 'Nachricht'],
]],
]]], null, 'nodes');
$message = $result['errors']['nodes[0].modules[0].modules[0][0].items[1].type'] ?? '';
self::assertStringContainsString('`type` ist hier der Knotentyp', $message);
self::assertStringContainsString('fieldItem.advanced.type', $message);
self::assertStringContainsString('"email"', $message);
self::assertStringNotContainsString('unbekanntes Modul', $message);
self::assertCount(1, $result['errors'], 'the explicit contact-field type is fine');
}
} }