feat: lit/divi-design-detach entkoppelt Variablen – Verweise in Seiten und Presets werden durch den Wert ersetzt (V9)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6e9218a2bc
commit
e951cc1353
@@ -21,27 +21,32 @@ use WP_Error;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* lit/divi-design-detach (spec §6.8, ruling R10 – presets only): removes every explicit reference to one preset –
|
* lit/divi-design-detach (spec §6.8, rulings R10, V9): removes every explicit reference to one preset or one global
|
||||||
* from the modulePreset/groupPreset stacks of pages, library entries and Theme Builder layouts (DetachRewrite through
|
* variable from pages, library entries and Theme Builder layouts (DetachRewrite or VariableDetachRewrite through the
|
||||||
* the PageRewriter) and, for a group preset without post_ids, from the groupPresets of other presets (DesignChange,
|
* PageRewriter) and, without post_ids, from presets that nest it (DesignChange, snapshot "presets"): a group preset
|
||||||
* snapshot "presets"). Modules fall back to the next preset of their stack or the default. The dry run is ONE
|
* leaves the groupPresets of other presets, a variable reference in a preset becomes the variable's value. Modules
|
||||||
* confirm_required with the pages and, when other presets nest it, the diff and design_rev of the preset step.
|
* fall back to the next preset of their stack or the default; a variable reference becomes the value (the variable
|
||||||
* Confirmed, the preset step runs first – its plan carries the page usages, so a blocked live post refuses before
|
* itself stays as it is). The dry run is ONE confirm_required with the pages and, when presets nest the target, the
|
||||||
* anything is written (P8a) – then at most PageRewriter::PAGE_LIMIT pages. A default preset keeps working implicitly
|
* diff and design_rev of the preset step. Confirmed, the preset step runs first – its plan carries the page usages,
|
||||||
* (Divi applies it); only explicit references go. Variables follow with Etappe 4c.
|
* so a blocked live post refuses before anything is written (P8a) – then at most PageRewriter::PAGE_LIMIT pages. A
|
||||||
|
* default preset keeps working implicitly (Divi applies it); only explicit references go. Global colors have no
|
||||||
|
* detach (ruling V9: they are only deactivated).
|
||||||
*/
|
*/
|
||||||
final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
||||||
{
|
{
|
||||||
private const VARIABLES = 'Variablen folgen mit Etappe 4c.';
|
|
||||||
|
|
||||||
private const DELETE_HINT = 'Jetzt kann lit/divi-presets-delete das Preset löschen.';
|
private const DELETE_HINT = 'Jetzt kann lit/divi-presets-delete das Preset löschen.';
|
||||||
|
|
||||||
private const DEFAULT_NOTE = '„%s“ ist das Standard-Preset von %s: Divi wendet es weiter auf Module ohne eigenes '
|
private const DEFAULT_NOTE = '„%s“ ist das Standard-Preset von %s: Divi wendet es weiter auf Module ohne eigenes '
|
||||||
. 'Preset an; entfernt werden nur die ausdrücklichen Verweise.';
|
. 'Preset an; entfernt werden nur die ausdrücklichen Verweise.';
|
||||||
|
|
||||||
|
private const ARCHIVE_HINT = 'Jetzt kann lit/divi-variables-set die Variable archivieren (status: archived).';
|
||||||
|
|
||||||
|
private const NO_COLORS = 'Globale Farben lassen sich nicht entkoppeln, nur deaktivieren (lit/divi-colors-set).';
|
||||||
|
|
||||||
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
|
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DesignSystemWriter $writer,
|
private DesignSystemWriter $writer,
|
||||||
|
private GlobalVariablesAdapter $variables,
|
||||||
private DesignChange $change,
|
private DesignChange $change,
|
||||||
private GlobalUsages $usages,
|
private GlobalUsages $usages,
|
||||||
private PageRewriter $pages,
|
private PageRewriter $pages,
|
||||||
@@ -57,23 +62,26 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
|||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return 'Divi-Preset-Verweise entfernen';
|
return 'Divi-Preset- und Variablen-Verweise entfernen';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function description(): string
|
public function description(): string
|
||||||
{
|
{
|
||||||
return 'Entfernt alle Verweise auf ein Preset (preset: module bzw. group + preset als Name oder ID) aus '
|
return 'Entfernt alle Verweise auf ein Preset (preset: module bzw. group + preset als Name oder ID) oder auf '
|
||||||
. 'Seiten, Bibliothekseinträgen und Theme-Builder-Layouts und – bei Gruppen-Presets – aus anderen Presets; '
|
. 'eine globale Variable (variable: Name oder ID) aus Seiten, Bibliothekseinträgen und '
|
||||||
. 'Module fallen auf das nächste Preset im Stapel bzw. das Standard-Preset zurück. post_ids beschränkt auf '
|
. 'Theme-Builder-Layouts und – bei Gruppen-Presets und Variablen – aus Presets; Module fallen auf das '
|
||||||
. 'diese Seiten (Verweise in Presets bleiben dann). Zuerst Probelauf (pages; bei Verweisen in Presets diff '
|
. 'nächste Preset im Stapel bzw. das Standard-Preset zurück, ein Variablenverweis wird durch den aktuellen '
|
||||||
. 'und design_rev), nach Rückfrage beim Nutzer confirm: true (mit design_rev, wenn der Probelauf eine '
|
. 'Wert der Variablen ersetzt (die Variable selbst bleibt; Verweise mitten in einem längeren Text bleiben '
|
||||||
. 'nennt); je Aufruf höchstens ' . PageRewriter::PAGE_LIMIT . ' Seiten, remaining nennt den Rest. Mit '
|
. 'stehen und stehen als Hinweis in warnings). post_ids beschränkt auf diese Seiten (Verweise in Presets '
|
||||||
. 'post_ids können nur die genannten Seiten das Schreiben blockieren und die Obergrenze für sehr viele '
|
. 'bleiben dann). Zuerst Probelauf (pages; bei Verweisen in Presets diff und design_rev), nach Rückfrage '
|
||||||
. 'Seiten gilt nicht; eine genannte veröffentlichte, private oder geplante Seite ohne Bearbeitungsrecht '
|
. 'beim Nutzer confirm: true (mit design_rev, wenn der Probelauf eine nennt); je Aufruf höchstens '
|
||||||
. 'verweigert den ganzen Aufruf (forbidden, blocked_ids), eine genannte andere Seite ohne '
|
. PageRewriter::PAGE_LIMIT . ' Seiten, remaining nennt den Rest. Mit post_ids können nur die genannten '
|
||||||
. 'Bearbeitungsrecht steht in skipped (bei mehr als 2000 Live-Fundstellen entfällt die Vorabprüfung, '
|
. 'Seiten das Schreiben blockieren und die Obergrenze für sehr viele Seiten gilt nicht; eine genannte '
|
||||||
. 'dann auch eine genannte Live-Seite). Danach kann lit/divi-presets-delete das Preset löschen. '
|
. 'veröffentlichte, private oder geplante Seite ohne Bearbeitungsrecht verweigert den ganzen Aufruf '
|
||||||
. self::VARIABLES;
|
. '(forbidden, blocked_ids), eine genannte andere Seite ohne Bearbeitungsrecht steht in skipped (bei mehr '
|
||||||
|
. 'als 2000 Live-Fundstellen entfällt die Vorabprüfung, dann auch eine genannte Live-Seite). Danach kann '
|
||||||
|
. 'lit/divi-presets-delete das Preset löschen bzw. lit/divi-variables-set die Variable archivieren. '
|
||||||
|
. self::NO_COLORS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function inputSchema(): array
|
public function inputSchema(): array
|
||||||
@@ -90,7 +98,10 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
|||||||
],
|
],
|
||||||
'required' => ['preset'],
|
'required' => ['preset'],
|
||||||
],
|
],
|
||||||
'variable' => ['type' => 'string', 'description' => self::VARIABLES],
|
'variable' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Name oder ID einer globalen Variablen (statt preset). ' . self::NO_COLORS,
|
||||||
|
],
|
||||||
'post_ids' => PostIdsInput::schema(),
|
'post_ids' => PostIdsInput::schema(),
|
||||||
] + DesignChange::inputProperties(),
|
] + DesignChange::inputProperties(),
|
||||||
'additionalProperties' => false,
|
'additionalProperties' => false,
|
||||||
@@ -117,11 +128,12 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Like lit/divi-presets-assign (it rewrites every kind of post), plus the preset step that removes references
|
* Like lit/divi-presets-assign (it rewrites every kind of post), plus the preset step that removes references
|
||||||
* nested in other presets (PresetWrite).
|
* nested in presets (PresetWrite); a variable is read and its rights checked through GlobalVariablesAdapter
|
||||||
|
* (VariableWrite).
|
||||||
*/
|
*/
|
||||||
public function diviFeatures(array $input): array
|
public function diviFeatures(array $input): array
|
||||||
{
|
{
|
||||||
return [
|
$features = [
|
||||||
DiviFeature::PageRead,
|
DiviFeature::PageRead,
|
||||||
DiviFeature::PageWrite,
|
DiviFeature::PageWrite,
|
||||||
DiviFeature::Catalog,
|
DiviFeature::Catalog,
|
||||||
@@ -132,26 +144,25 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
|||||||
DiviFeature::ThemeBuilderWrite,
|
DiviFeature::ThemeBuilderWrite,
|
||||||
DiviFeature::Roles,
|
DiviFeature::Roles,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return array_key_exists('variable', $input) ? [...$features, DiviFeature::VariableWrite] : $features;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function permission(array $input): bool|WP_Error
|
public function permission(array $input): bool|WP_Error
|
||||||
{
|
{
|
||||||
return DesignRights::presets($this->writer) ?? true;
|
return $this->rights($input) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(array $input): mixed
|
public function execute(array $input): mixed
|
||||||
{
|
{
|
||||||
$refusal = DesignRights::presets($this->writer);
|
$refusal = $this->rights($input);
|
||||||
if ($refusal !== null) {
|
if ($refusal !== null) {
|
||||||
return $refusal;
|
return $refusal;
|
||||||
}
|
}
|
||||||
if (array_key_exists('variable', $input)) {
|
if (array_key_exists('variable', $input) && array_key_exists('preset', $input)) {
|
||||||
return Errors::unsupported(
|
return self::invalid(['variable' => 'entweder preset oder variable angeben, nicht beides']);
|
||||||
'Verweise auf Variablen entfernt LIT MCP noch nicht; nichts wurde geändert.',
|
|
||||||
self::VARIABLES . ' Bis dahin nur Presets angeben (preset).'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$target = $this->presetTarget($input);
|
$target = array_key_exists('variable', $input) ? $this->variableTarget($input) : $this->presetTarget($input);
|
||||||
if ($target instanceof WP_Error) {
|
if ($target instanceof WP_Error) {
|
||||||
return $target;
|
return $target;
|
||||||
}
|
}
|
||||||
@@ -159,6 +170,88 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
|||||||
return ($input['confirm'] ?? false) === true ? $this->confirmed($input, $target) : $this->dryRun($target);
|
return ($input['confirm'] ?? false) === true ? $this->confirmed($input, $target) : $this->dryRun($target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spec §10: presets need the preset rights; a variable also the variable rights – the preset step writes presets
|
||||||
|
* either way (V9).
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
private function rights(array $input): ?WP_Error
|
||||||
|
{
|
||||||
|
$refusal = DesignRights::presets($this->writer);
|
||||||
|
if ($refusal !== null || ! array_key_exists('variable', $input)) {
|
||||||
|
return $refusal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DesignRights::variables($this->writer, $this->variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The global variable to detach, resolved on the live inventory; its current value replaces the references
|
||||||
|
* (V9: whether it is active or archived does not matter).
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
private function variableTarget(array $input): DetachTarget|WP_Error
|
||||||
|
{
|
||||||
|
$needle = is_string($input['variable'] ?? null) ? trim($input['variable']) : '';
|
||||||
|
if ($needle === '') {
|
||||||
|
return self::invalid(['variable' => 'Name oder ID der Variablen erwartet']);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$postIds = PostIdsInput::parse($input['post_ids'] ?? null);
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
return self::invalid(['post_ids' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
if (str_starts_with($needle, 'gcid-')) {
|
||||||
|
return Errors::unsupported(
|
||||||
|
sprintf('„%s“ ist eine globale Farbe; nichts wurde geändert.', $needle),
|
||||||
|
self::NO_COLORS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$book = VariableBook::fromDivi($this->variables->read());
|
||||||
|
try {
|
||||||
|
$found = $book->find($needle);
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
return self::invalid(['variable' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
if ($found === null) {
|
||||||
|
return Errors::notFound(
|
||||||
|
sprintf('Unbekannte Variable „%s“; nichts wurde geändert.', $needle),
|
||||||
|
'Vorhandene Variablen mit Namen und IDs liefert lit/divi-design-system (direkt: '
|
||||||
|
. 'lit-divi-design-system). ' . self::NO_COLORS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $found['id'];
|
||||||
|
$type = $found['type'];
|
||||||
|
$entry = $book->entry($type, $id) ?? [];
|
||||||
|
$label = is_string($entry['label'] ?? null) && $entry['label'] !== '' ? $entry['label'] : $id;
|
||||||
|
$value = $entry['value'] ?? null;
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return Errors::unsupported(
|
||||||
|
sprintf('Variable „%s“ (Typ %s) hat keinen Textwert; nichts wurde geändert.', $label, $type),
|
||||||
|
'Verweise auf diese Variable im Divi-Builder lösen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$nested = $postIds === null && PresetBook::fromDivi($this->writer->presets())->referencing($id) !== [];
|
||||||
|
$affixes = $type === 'strings';
|
||||||
|
|
||||||
|
return new DetachTarget(
|
||||||
|
sprintf('Variable „%s“', $label),
|
||||||
|
$postIds,
|
||||||
|
new ReferenceUsageQuery([$id]),
|
||||||
|
static fn (): NodeRewrite => new VariableDetachRewrite($id, $value, $type),
|
||||||
|
$nested
|
||||||
|
? static fn (PresetBook $presets): PresetBook => $presets->withVariableResolved($id, $value, $affixes)
|
||||||
|
: null,
|
||||||
|
'Presets',
|
||||||
|
[],
|
||||||
|
self::ARCHIVE_HINT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The preset to detach, resolved on the live inventory.
|
* The preset to detach, resolved on the live inventory.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use LIT\MCP\Integrations\Divi\Design\DiviPresetGetAbility;
|
|||||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignRestoreAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviDesignRestoreAbility;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignSnapshotsAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviDesignSnapshotsAbility;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DiviPresetsAssignAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviPresetsAssignAbility;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
|
||||||
@@ -274,6 +275,7 @@ final class DiviIntegration implements Integration, Bootable
|
|||||||
),
|
),
|
||||||
new DiviDesignDetachAbility(
|
new DiviDesignDetachAbility(
|
||||||
$this->designWriter,
|
$this->designWriter,
|
||||||
|
new GlobalVariablesAdapter(),
|
||||||
$this->designChange(),
|
$this->designChange(),
|
||||||
$this->globalUsages(),
|
$this->globalUsages(),
|
||||||
$this->pageRewriter($loader),
|
$this->pageRewriter($loader),
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ pass "mass assignment: dry run lists 2 pages, confirm saved 2 (remaining 0), sec
|
|||||||
# 6. detach of the group preset: it is in use, a stale design_rev is refused, then dry run, confirm, delete.
|
# 6. detach of the group preset: it is in use, a stale design_rev is refused, then dry run, confirm, delete.
|
||||||
GROUP_ITEM=$(jq -cn --arg g "$PRESET_G" '{items: [{group: "font", preset: $g}]}')
|
GROUP_ITEM=$(jq -cn --arg g "$PRESET_G" '{items: [{group: "font", preset: $g}]}')
|
||||||
[ "$(error_code "$(ability lit/divi-presets-delete "$GROUP_ITEM")")" = "in_use" ] || fail "delete of a used group preset"
|
[ "$(error_code "$(ability lit/divi-presets-delete "$GROUP_ITEM")")" = "in_use" ] || fail "delete of a used group preset"
|
||||||
UNSUPPORTED=$(ability lit/divi-design-detach '{"variable": "Irgendeine Variable"}')
|
UNKNOWN_VARIABLE=$(ability lit/divi-design-detach '{"variable": "Irgendeine Variable"}')
|
||||||
[ "$(error_code "$UNSUPPORTED")" = "unsupported" ] || fail "detach of a variable: $UNSUPPORTED"
|
[ "$(error_code "$UNKNOWN_VARIABLE")" = "not_found" ] || fail "detach of an unknown variable: $UNKNOWN_VARIABLE"
|
||||||
DETACH=$(jq -cn --arg g "$PRESET_G" '{preset: {group: "font", preset: $g}}')
|
DETACH=$(jq -cn --arg g "$PRESET_G" '{preset: {group: "font", preset: $g}}')
|
||||||
DRY=$(timed "detach dry run" ability lit/divi-design-detach "$DETACH")
|
DRY=$(timed "detach dry run" ability lit/divi-design-detach "$DETACH")
|
||||||
[ "$(error_code "$DRY")" = "confirm_required" ] || fail "detach without confirm: $DRY"
|
[ "$(error_code "$DRY")" = "confirm_required" ] || fail "detach without confirm: $DRY"
|
||||||
@@ -227,7 +227,7 @@ STALE=$(ability lit/divi-design-detach "$(echo "$DETACH" | jq -c '. + {confirm:
|
|||||||
[ "$(error_code "$STALE")" = "rev_mismatch" ] || fail "stale design_rev: $STALE"
|
[ "$(error_code "$STALE")" = "rev_mismatch" ] || fail "stale design_rev: $STALE"
|
||||||
nested_groups "$PRESET_B" | jq -e 'length > 0' >/dev/null || fail "stale detach changed preset B"
|
nested_groups "$PRESET_B" | jq -e 'length > 0' >/dev/null || fail "stale detach changed preset B"
|
||||||
button_attrs "$PAGE1" | jq -e '(.groupPreset // null) != null' >/dev/null || fail "stale detach changed page 1"
|
button_attrs "$PAGE1" | jq -e '(.groupPreset // null) != null' >/dev/null || fail "stale detach changed page 1"
|
||||||
pass "group preset is in use, variable detach unsupported, stale design_rev refused without writing"
|
pass "group preset is in use, unknown variable not found, stale design_rev refused without writing"
|
||||||
|
|
||||||
echo "info - preset B before detach: $(preset_item "$PRESET_B")" >&2
|
echo "info - preset B before detach: $(preset_item "$PRESET_B")" >&2
|
||||||
DETACHED=$(timed "detach confirmed" ability lit/divi-design-detach "$(echo "$DETACH" | jq -c --arg r "$DETACH_REV" '. + {confirm: true, design_rev: $r}')")
|
DETACHED=$(timed "detach confirmed" ability lit/divi-design-detach "$(echo "$DETACH" | jq -c --arg r "$DETACH_REV" '. + {confirm: true, design_rev: $r}')")
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ use Brain\Monkey\Functions;
|
|||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Abilities\Errors;
|
use LIT\MCP\Abilities\Errors;
|
||||||
use LIT\MCP\Abilities\WriteFailed;
|
use LIT\MCP\Abilities\WriteFailed;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignChange;
|
use LIT\MCP\Integrations\Divi\Design\DesignChange;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DetachRewrite;
|
use LIT\MCP\Integrations\Divi\Design\DetachRewrite;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
|
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
|
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||||||
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
|
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ReferenceUsageQuery;
|
||||||
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\VariableDetachRewrite;
|
||||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||||
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
@@ -33,6 +37,10 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
|||||||
{
|
{
|
||||||
private const LESETEXT = ['group' => 'font-body', 'preset' => 'Lesetext'];
|
private const LESETEXT = ['group' => 'font-body', 'preset' => 'Lesetext'];
|
||||||
|
|
||||||
|
private const RADIUS = 'gvid-radius0001';
|
||||||
|
|
||||||
|
private const CLAIM = 'gvid-claim00001';
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@@ -48,13 +56,15 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
|||||||
?SnapshotStore $snapshots = null,
|
?SnapshotStore $snapshots = null,
|
||||||
?FakeChangeLog $log = null,
|
?FakeChangeLog $log = null,
|
||||||
?UsageReport $report = null,
|
?UsageReport $report = null,
|
||||||
bool $unfiltered = true
|
bool $unfiltered = true,
|
||||||
|
?GlobalVariablesAdapter $variables = null
|
||||||
): DiviDesignDetachAbility {
|
): DiviDesignDetachAbility {
|
||||||
$usages = Mockery::mock(GlobalUsages::class);
|
$usages = Mockery::mock(GlobalUsages::class);
|
||||||
$usages->allows('reportFor')->andReturn($report ?? new UsageReport(0, [], 0, 0, [], 0, false));
|
$usages->allows('reportFor')->andReturn($report ?? new UsageReport(0, [], 0, 0, [], 0, false));
|
||||||
|
|
||||||
return new DiviDesignDetachAbility(
|
return new DiviDesignDetachAbility(
|
||||||
$writer ?? self::readOnlyWriter(),
|
$writer ?? self::readOnlyWriter(),
|
||||||
|
$variables ?? self::variableAdapter(),
|
||||||
new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), $log ?? FakeChangeLog::forbidden()),
|
new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), $log ?? FakeChangeLog::forbidden()),
|
||||||
$usages,
|
$usages,
|
||||||
$pages,
|
$pages,
|
||||||
@@ -77,6 +87,64 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
|||||||
return $writer;
|
return $writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reads variables() (the Variablen-Verwaltung is allowed unless $manage is false), never saves. */
|
||||||
|
private static function variableAdapter(bool $manage = true): GlobalVariablesAdapter&MockInterface
|
||||||
|
{
|
||||||
|
$variables = Mockery::mock(GlobalVariablesAdapter::class);
|
||||||
|
$variables->allows('read')->andReturn(self::variables());
|
||||||
|
$variables->allows('canManage')->andReturn($manage);
|
||||||
|
$variables->expects('save')->never();
|
||||||
|
|
||||||
|
return $variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<string, array<string, mixed>>> Global variables in Divi's stored form. */
|
||||||
|
private static function variables(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'numbers' => [self::RADIUS => [
|
||||||
|
'id' => self::RADIUS, 'label' => 'Eckenradius', 'value' => '8px', 'order' => '1',
|
||||||
|
'status' => 'active', 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => 'numbers',
|
||||||
|
]],
|
||||||
|
// Archived or not does not matter for a detach (V9).
|
||||||
|
'strings' => [self::CLAIM => [
|
||||||
|
'id' => self::CLAIM, 'label' => 'Claim', 'value' => 'Gut beraten', 'order' => '',
|
||||||
|
'status' => 'archived', 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => 'strings',
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> PresetBookTest::stored() with the radius variable in preset "CTA primär". */
|
||||||
|
private static function storedWithVariable(): array
|
||||||
|
{
|
||||||
|
$stored = PresetBookTest::stored();
|
||||||
|
$radius = ['topLeft' => VariableDetachRewriteTest::ref(self::RADIUS)];
|
||||||
|
$stored['module']['divi/button']['items']['aaaaaaaaaa']['attrs'] = ['button' => ['decoration' => [
|
||||||
|
'border' => ['desktop' => ['value' => ['radius' => $radius]]],
|
||||||
|
]]];
|
||||||
|
|
||||||
|
return $stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Matches the reference query of a preview()/apply() call: it finds $id and nothing else. */
|
||||||
|
private static function referencing(string $id): Closure
|
||||||
|
{
|
||||||
|
return static fn (ReferenceUsageQuery $query): bool => $query->matches(
|
||||||
|
'<!-- wp:divi/text {"x":"' . $id . '"} /-->'
|
||||||
|
) && ! $query->matches('<!-- wp:divi/text {"x":"gvid-andere0001"} /-->');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Matches a VariableDetachRewrite strategy that turns a whole reference to $id into $expected. */
|
||||||
|
private static function resolving(string $id, string $expected, array $settings = []): Closure
|
||||||
|
{
|
||||||
|
return static function (Closure $rewrite) use ($id, $expected, $settings): bool {
|
||||||
|
$strategy = $rewrite();
|
||||||
|
$block = Block::create('divi/text', ['x' => VariableDetachRewriteTest::ref($id, $settings)]);
|
||||||
|
|
||||||
|
return $strategy instanceof VariableDetachRewrite && $strategy->rewrite($block) === ['x' => $expected];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static function untouched(): PageRewriter&MockInterface
|
private static function untouched(): PageRewriter&MockInterface
|
||||||
{
|
{
|
||||||
$pages = Mockery::mock(PageRewriter::class);
|
$pages = Mockery::mock(PageRewriter::class);
|
||||||
@@ -110,14 +178,158 @@ final class DiviDesignDetachAbilityTest extends TestCase
|
|||||||
self::assertToolError($ability->execute(['preset' => self::LESETEXT]), 'forbidden');
|
self::assertToolError($ability->execute(['preset' => self::LESETEXT]), 'forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testVariablesFollowWithStage4c(): void
|
public function testAVariableNeedsTheVariableRightsToo(): void
|
||||||
{
|
{
|
||||||
$error = self::assertToolError(
|
$ability = $this->ability(self::untouched(), null, null, null, null, true, self::variableAdapter(false));
|
||||||
$this->ability(self::untouched())->execute(['variable' => 'Eckenradius']),
|
|
||||||
'unsupported'
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertStringContainsString('Variablen folgen mit Etappe 4c.', $error['hint']);
|
self::assertToolError($ability->permission(['variable' => 'Eckenradius']), 'forbidden');
|
||||||
|
self::assertToolError($ability->execute(['variable' => 'Eckenradius']), 'forbidden');
|
||||||
|
self::assertTrue($ability->permission(['preset' => self::LESETEXT]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownAmbiguousAndColorVariables(): void
|
||||||
|
{
|
||||||
|
$ability = $this->ability(self::untouched());
|
||||||
|
|
||||||
|
$unknown = self::assertToolError($ability->execute(['variable' => 'Fehlt']), 'not_found');
|
||||||
|
self::assertStringContainsString('lit/divi-colors-set', $unknown['hint']);
|
||||||
|
$color = self::assertToolError($ability->execute(['variable' => 'gcid-primary-color']), 'unsupported');
|
||||||
|
self::assertStringContainsString('nur deaktivieren', $color['hint']);
|
||||||
|
self::assertArrayHasKey('variable', self::errorFields($ability->execute(['variable' => ' '])));
|
||||||
|
$both = $ability->execute(['variable' => 'Eckenradius', 'preset' => self::LESETEXT]);
|
||||||
|
self::assertSame('entweder preset oder variable angeben, nicht beides', self::errorFields($both)['variable']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariableDryRunListsPagesAndThePresetsThatUseIt(): void
|
||||||
|
{
|
||||||
|
$pages = Mockery::mock(PageRewriter::class);
|
||||||
|
$pages->expects('preview')
|
||||||
|
->with(
|
||||||
|
Mockery::on(self::referencing(self::RADIUS)),
|
||||||
|
Mockery::on(self::resolving(self::RADIUS, '8px')),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
->andReturn(PageRewriteReportTest::preview(1));
|
||||||
|
$pages->expects('apply')->never();
|
||||||
|
$ability = $this->ability($pages, self::readOnlyWriter(self::storedWithVariable()));
|
||||||
|
|
||||||
|
$dry = self::assertToolError($ability->execute(['variable' => 'eckenradius']), 'confirm_required');
|
||||||
|
|
||||||
|
self::assertSame(12, $dry['pages'][0]['post_id']);
|
||||||
|
self::assertSame([['aaaaaaaaaa', 'changed']], array_map(
|
||||||
|
static fn (array $row): array => [$row['id'], $row['action']],
|
||||||
|
$dry['diff']
|
||||||
|
));
|
||||||
|
self::assertSame(PresetBook::fromDivi(self::storedWithVariable())->rev(), $dry['design_rev']);
|
||||||
|
self::assertStringStartsWith('Entkoppeln von Variable „Eckenradius“ – betroffen', $dry['message']);
|
||||||
|
self::assertStringContainsString('die Verweise in 1 Presets (diff, mit Snapshot)', $dry['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAStringsVariableKeepsBeforeAndAfterAndArchivedOnesWork(): void
|
||||||
|
{
|
||||||
|
$pages = Mockery::mock(PageRewriter::class);
|
||||||
|
$pages->expects('preview')
|
||||||
|
->with(
|
||||||
|
Mockery::on(self::referencing(self::CLAIM)),
|
||||||
|
Mockery::on(self::resolving(self::CLAIM, '„Gut beraten“', ['before' => '„', 'after' => '“'])),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
->andReturn(PageRewriteReportTest::preview(1));
|
||||||
|
|
||||||
|
$dry = self::assertToolError($this->ability($pages)->execute(['variable' => self::CLAIM]), 'confirm_required');
|
||||||
|
|
||||||
|
self::assertArrayNotHasKey('diff', $dry, 'no preset uses the claim');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariableConfirmResolvesThePresetsFirstThenThePages(): void
|
||||||
|
{
|
||||||
|
$calls = [];
|
||||||
|
$stored = self::storedWithVariable();
|
||||||
|
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||||
|
$writer->allows('presets')->andReturn($stored);
|
||||||
|
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||||
|
$writer->expects('savePresets')
|
||||||
|
->with(
|
||||||
|
Mockery::on(static fn (array $rest): bool => ($rest['module'][0]['items'][0]['attrs']['button']
|
||||||
|
['decoration']['border']['desktop']['value']['radius']['topLeft'] ?? null) === '8px'),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
->andReturnUsing(static function () use (&$calls, $stored): array {
|
||||||
|
$calls[] = 'presets';
|
||||||
|
|
||||||
|
return PresetBook::fromDivi($stored)->withVariableResolved(self::RADIUS, '8px')->data();
|
||||||
|
});
|
||||||
|
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||||
|
$snapshots->expects('save')->with('presets', $stored, 'lit/divi-design-detach')->andReturn('presets-5');
|
||||||
|
$snapshots->expects('attachLog');
|
||||||
|
$pages = Mockery::mock(PageRewriter::class);
|
||||||
|
$pages->expects('apply')
|
||||||
|
->with(
|
||||||
|
'lit/divi-design-detach',
|
||||||
|
Mockery::on(self::referencing(self::RADIUS)),
|
||||||
|
Mockery::on(self::resolving(self::RADIUS, '8px')),
|
||||||
|
null,
|
||||||
|
'Entkoppeln von Variable „Eckenradius“'
|
||||||
|
)
|
||||||
|
->andReturnUsing(static function () use (&$calls): array {
|
||||||
|
$calls[] = 'pages';
|
||||||
|
|
||||||
|
return PageRewriteReportTest::applied(1, 0);
|
||||||
|
});
|
||||||
|
$ability = $this->ability($pages, $writer, $snapshots, new FakeChangeLog(9));
|
||||||
|
|
||||||
|
$result = $ability->execute([
|
||||||
|
'variable' => self::RADIUS,
|
||||||
|
'confirm' => true,
|
||||||
|
'design_rev' => PresetBook::fromDivi($stored)->rev(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(['presets', 'pages'], $calls);
|
||||||
|
self::assertSame('presets-5', $result['snapshot_id']);
|
||||||
|
self::assertSame('aaaaaaaaaa', $result['diff'][0]['id']);
|
||||||
|
self::assertStringStartsWith('Presets: 1 geändert.', $result['summary']);
|
||||||
|
self::assertStringEndsWith(
|
||||||
|
'Jetzt kann lit/divi-variables-set die Variable archivieren (status: archived).',
|
||||||
|
$result['summary']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariablePostIdsSkipThePresetStep(): void
|
||||||
|
{
|
||||||
|
$pages = Mockery::mock(PageRewriter::class);
|
||||||
|
$pages->expects('apply')
|
||||||
|
->with('lit/divi-design-detach', Mockery::any(), Mockery::any(), [12], Mockery::type('string'))
|
||||||
|
->andReturn(PageRewriteReportTest::applied(1, 0));
|
||||||
|
$ability = $this->ability($pages, self::readOnlyWriter(self::storedWithVariable()));
|
||||||
|
|
||||||
|
$result = $ability->execute(['variable' => 'Eckenradius', 'post_ids' => [12], 'confirm' => true]);
|
||||||
|
|
||||||
|
self::assertSame([], $result['diff']);
|
||||||
|
self::assertNull($result['snapshot_id']);
|
||||||
|
self::assertStringNotContainsString('lit/divi-variables-set', $result['summary']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoVariableReferenceLeftPointsToArchiving(): void
|
||||||
|
{
|
||||||
|
$pages = Mockery::mock(PageRewriter::class);
|
||||||
|
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(0));
|
||||||
|
|
||||||
|
$result = $this->ability($pages)->execute(['variable' => 'Eckenradius']);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
'Keine Verweise auf Variable „Eckenradius“ gefunden – nichts zu tun. Jetzt kann lit/divi-variables-set '
|
||||||
|
. 'die Variable archivieren (status: archived).',
|
||||||
|
$result['summary']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFeaturesPerBranch(): void
|
||||||
|
{
|
||||||
|
$ability = $this->ability(self::untouched());
|
||||||
|
|
||||||
|
self::assertNotContains(DiviFeature::VariableWrite, $ability->diviFeatures(['preset' => self::LESETEXT]));
|
||||||
|
self::assertContains(DiviFeature::VariableWrite, $ability->diviFeatures(['variable' => 'Eckenradius']));
|
||||||
|
self::assertContains(DiviFeature::PresetWrite, $ability->diviFeatures(['variable' => 'Eckenradius']));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUnknownAndAmbiguousPresets(): void
|
public function testUnknownAndAmbiguousPresets(): void
|
||||||
|
|||||||
@@ -173,6 +173,13 @@ final class DiviToolFeaturesTest extends TestCase
|
|||||||
'lit/divi-presets-set', ['items' => [1, ['from_node' => ['post_id' => $library, 'id' => 's0']]]],
|
'lit/divi-presets-set', ['items' => [1, ['from_node' => ['post_id' => $library, 'id' => 's0']]]],
|
||||||
['design-read', 'preset-write', 'catalog', 'page-read', 'roles'],
|
['design-read', 'preset-write', 'catalog', 'page-read', 'roles'],
|
||||||
],
|
],
|
||||||
|
'design-detach of a variable' => [
|
||||||
|
'lit/divi-design-detach', ['variable' => 'Eckenradius'],
|
||||||
|
[
|
||||||
|
'page-read', 'page-write', 'catalog', 'design-read', 'preset-write', 'library-read',
|
||||||
|
'library-write', 'tb-write', 'roles', 'variable-write',
|
||||||
|
],
|
||||||
|
],
|
||||||
'library-list' => ['lit/library-list', [], ['library-read', 'roles']],
|
'library-list' => ['lit/library-list', [], ['library-read', 'roles']],
|
||||||
'library-list of one entry' => [
|
'library-list of one entry' => [
|
||||||
'lit/library-list', ['id' => $library], ['library-read', 'roles', 'page-read', 'design-read'],
|
'lit/library-list', ['id' => $library], ['library-read', 'roles', 'page-read', 'design-read'],
|
||||||
|
|||||||
Reference in New Issue
Block a user