fix: lit/divi-presets-assign verweigert leeren Text als Preset (Eingabe und Gruppen); Stapelgrenze und Preset-Schema aus PresetAssignment

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 17:36:45 +02:00
co-authored by Claude Sonnet 5.5
parent 6b5de199f9
commit b805ed64dd
4 changed files with 69 additions and 39 deletions
@@ -25,12 +25,11 @@ defined('ABSPATH') || exit;
*/
final class DiviPresetsAssignAbility implements Ability
{
public const MAX_STACK = 10;
/** Path prefix for PresetAssignment's errors; removed again so fields name the input keys. */
private const ROOT = 'input';
private const USE_DETACH = 'null entfernt site-weit nichts; Preset-Verweise entfernt lit/divi-design-detach';
private const USE_DETACH = 'null und leerer Text entfernen site-weit nichts; '
. 'Preset-Verweise entfernt lit/divi-design-detach';
/** @param Closure(): PresetAssignment $assignment A fresh assignment (own preset book) per call. */
public function __construct(
@@ -70,18 +69,16 @@ final class DiviPresetsAssignAbility implements Ability
'type' => 'object',
'properties' => [
'module' => ['type' => 'string', 'description' => 'Modul, z. B. "button" oder "divi/button".'],
'preset' => [
'type' => ['string', 'array'],
'items' => ['type' => 'string'],
'minItems' => 1,
'maxItems' => self::MAX_STACK,
'description' => 'Modul-Preset (Name oder ID) oder Stapel; bei gleicher Priorität gewinnt das '
. 'letzte Preset im Stapel (Divi-Verhalten).',
],
'preset' => PresetAssignment::valueSchema(
false,
'Modul-Preset (Name oder ID) oder Stapel; bei gleicher Priorität gewinnt das '
. 'letzte Preset im Stapel (Divi-Verhalten).'
),
'group_presets' => [
'type' => 'object',
'description' => '{"<Gruppe>": "<Name>" | ["<Name>", …]}; Gruppe als Kurzname (font-body), voller '
. 'Name (divi/font-body) oder Attributpfad.',
'additionalProperties' => PresetAssignment::valueSchema(false),
],
'post_ids' => PostIdsInput::schema(),
'only_without_preset' => [
@@ -224,7 +221,7 @@ final class DiviPresetsAssignAbility implements Ability
}
/**
* preset and group_presets as given; null removes nothing site-wide (lit/divi-design-detach does).
* preset and group_presets as given; null and "" remove nothing site-wide (lit/divi-design-detach does).
*
* @param array<string, mixed> $input
* @param array<string, string> $errors
@@ -236,7 +233,7 @@ final class DiviPresetsAssignAbility implements Ability
if ($spec === []) {
$errors['preset'] = 'preset oder group_presets angeben';
}
if (array_key_exists('preset', $spec) && $spec['preset'] === null) {
if (array_key_exists('preset', $spec) && in_array($spec['preset'], [null, ''], true)) {
$errors['preset'] = self::USE_DETACH;
}
$groups = $spec['group_presets'] ?? null;
@@ -244,7 +241,7 @@ final class DiviPresetsAssignAbility implements Ability
$errors['group_presets'] = 'mindestens eine Gruppe angeben';
}
foreach (is_array($groups) ? $groups : [] as $key => $value) {
if ($value === null) {
if (in_array($value, [null, ''], true)) {
$errors['group_presets.' . $key] = self::USE_DETACH;
}
}
@@ -19,7 +19,8 @@ defined('ABSPATH') || exit;
*/
final class PresetAssignment
{
private const MAX_STACK = 10;
/** The most presets one module (or group entry) can carry as a stack. */
public const MAX_STACK = 10;
private ?PresetBook $loaded = null;
@@ -60,6 +61,29 @@ final class PresetAssignment
return ['attrs' => $attrs, 'errors' => $errors];
}
/**
* JSON schema of a preset value: a name or ID, a stack of 1..MAX_STACK of them and, when $nullable, null.
* Without null (site-wide tools) empty strings are refused too: they would mean "remove", which only null or
* lit/divi-design-detach may do there. The single source for every tool that takes a preset value.
*
* @return array<string, mixed>
*/
public static function valueSchema(bool $nullable, string $description = ''): array
{
$schema = [
'type' => $nullable ? ['string', 'array', 'null'] : ['string', 'array'],
'items' => ['type' => 'string'],
'minItems' => 1,
'maxItems' => self::MAX_STACK,
];
if (! $nullable) {
$schema['minLength'] = 1;
$schema['items']['minLength'] = 1;
}
return $description === '' ? $schema : $schema + ['description' => $description];
}
/**
* The attributes without the local values that lie on values of the presets the block names (ruling R4).
*
@@ -8,6 +8,7 @@ use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\Compose\ColumnWidths;
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
use WP_Error;
defined('ABSPATH') || exit;
@@ -36,8 +37,8 @@ final class DiviUpdateAbility extends DiviWriteAbility
{
return 'Ändert vorhandene Divi-Knoten: changes = Liste von {id, set: {Kurzform-Felder}, attrs: {Attributpfad: '
. '{Breakpoint: Wert}}, preset, group_presets, clear_overrides, label}. null entfernt einen Wert. '
. 'preset = Name/ID oder Liste (Stapel, bis 10; bei gleicher Priorität gewinnt das letzte Preset im '
. 'Stapel); group_presets = {Gruppe: Preset}, z. B. {"font-body": "Fließtext"}. Überdecken lokale Werte '
. 'preset = Name/ID oder Liste (Stapel, bis ' . PresetAssignment::MAX_STACK . '; bei gleicher Priorität '
. 'gewinnt das letzte Preset im Stapel); group_presets = {Gruppe: Preset}, z. B. {"font-body": "Fließtext"}. Überdecken lokale Werte '
. 'ein Preset, meldet die Antwort das; clear_overrides: true entfernt sie (nach set/attrs). Nicht '
. 'genannte Einstellungen bleiben unverändert. Jeder Knoten nur einmal pro Aufruf; denselben Pfad nicht '
. 'zugleich über set und attrs. tree_rev aus lit-divi-outline ist Pflicht; alles oder nichts.';
@@ -61,16 +62,18 @@ final class DiviUpdateAbility extends DiviWriteAbility
'type' => 'object',
'description' => 'Vollform: Attributpfad → Breakpoint-Werte.',
],
'preset' => self::presetValue(
'Preset-Name oder -ID, Liste (Stapel, 1–10) oder null zum Entfernen. Bei gleicher '
. 'Priorität gewinnt das letzte Preset im Stapel.'
'preset' => PresetAssignment::valueSchema(
true,
'Preset-Name oder -ID, Liste (Stapel, 1–' . PresetAssignment::MAX_STACK
. ') oder null zum Entfernen. Bei gleicher Priorität gewinnt das letzte '
. 'Preset im Stapel.'
),
'group_presets' => [
'type' => 'object',
'description' => 'Gruppen-Presets: {Gruppe: Preset-Name | Liste | null}. Gruppe = '
. 'Kurzname (font-body), voller Name (divi/font-body) oder Attributpfad; null '
. 'entfernt den Eintrag der Gruppe.',
'additionalProperties' => self::presetValue(),
'additionalProperties' => PresetAssignment::valueSchema(true),
],
'clear_overrides' => [
'type' => 'boolean',
@@ -172,21 +175,4 @@ final class DiviUpdateAbility extends DiviWriteAbility
return $change;
}
/**
* Schema of one preset value: a name or id, a stack of 1–10 of them, or null.
*
* @return array<string, mixed>
*/
private static function presetValue(string $description = ''): array
{
$schema = [
'type' => ['string', 'array', 'null'],
'items' => ['type' => 'string'],
'minItems' => 1,
'maxItems' => 10,
];
return $description === '' ? $schema : $schema + ['description' => $description];
}
}