feat: lit/divi-presets-delete – unbenutzte Presets löschen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-16 17:50:29 +02:00
co-authored by Claude Sonnet 5.5
parent 25720ad086
commit 2e8f723b01
6 changed files with 300 additions and 2 deletions
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use InvalidArgumentException;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/divi-presets-delete (spec §6.4, G5): deletes presets nothing uses any more – no page, library entry or Theme
* Builder layout (GlobalUsages with PresetUsageQuery), and no other preset that nests it. A used preset is refused
* with its usages (in_use); lit/divi-design-detach (Etappe 4b) removes the references first. A default preset loses
* its default. Through the change pipeline with Divi's delete semantics (legacy option cleaned).
*/
final class DiviPresetsDeleteAbility implements Ability
{
public const MAX_ITEMS = 20;
private const DETACH_HINT = 'Erst die Verweise entfernen: lit/divi-design-detach (ab Etappe 4b) oder im '
. 'Divi-Builder; danach erneut löschen.';
public function __construct(
private DesignSystemWriter $writer,
private DesignChange $change,
private GlobalUsages $usages
) {
}
public function name(): string
{
return 'lit/divi-presets-delete';
}
public function label(): string
{
return 'Divi-Presets löschen';
}
public function description(): string
{
return 'Löscht Modul- oder Gruppen-Presets (items: module bzw. group + preset als Name oder ID), die nirgends '
. 'mehr verwendet werden; sonst Fehler in_use mit den Fundstellen. Zuerst Probelauf, nach Rückfrage beim '
. 'Nutzer confirm: true mit design_rev; vorher wird ein Snapshot angelegt (lit/divi-design-restore).';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'items' => [
'type' => 'array',
'minItems' => 1,
'maxItems' => self::MAX_ITEMS,
'items' => [
'type' => 'object',
'properties' => [
'module' => ['type' => 'string'],
'group' => ['type' => 'string'],
'preset' => ['type' => 'string'],
],
'required' => ['preset'],
],
],
] + DesignChange::inputProperties(),
'required' => ['items'],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return DesignChange::outputSchema();
}
public function annotations(): array
{
return Annotations::destructive(true);
}
public function isDirect(): bool
{
return false;
}
public function permission(array $input): bool|WP_Error
{
return DesignRights::presets($this->writer) ?? true;
}
public function execute(array $input): mixed
{
$refusal = DesignRights::presets($this->writer);
if ($refusal !== null) {
return $refusal;
}
$items = $input['items'] ?? null;
if (! is_array($items) || ! array_is_list($items) || $items === [] || count($items) > self::MAX_ITEMS) {
return Errors::invalidInput(
'items fehlt oder ist zu lang; nichts wurde gelöscht.',
['items' => sprintf('1 bis %d Einträge', self::MAX_ITEMS)]
);
}
return $this->change->run(
$this->name(),
new PresetArea($this->writer),
$input,
fn (array $raw): DesignPlan|WP_Error => $this->plan(PresetBook::fromDivi($raw), $items)
);
}
/** @param list<mixed> $items */
private function plan(PresetBook $before, array $items): DesignPlan|WP_Error
{
$book = $before;
$errors = [];
$targets = [];
foreach ($items as $index => $item) {
$field = sprintf('items[%d]', $index);
try {
$scope = PresetScope::fromInput(is_array($item) ? $item : [], $field);
$found = $before->find($scope, is_string($item['preset'] ?? null) ? $item['preset'] : '');
} catch (InvalidArgumentException $e) {
$errors[$field] = $e->getMessage();
continue;
}
if ($found === null) {
$errors[$field . '.preset'] = sprintf('unbekanntes %s „%s“', $scope->label(), (string) $item['preset']);
continue;
}
$id = (string) $found['id'];
$targets[] = ['scope' => $scope, 'id' => $id, 'default' => $before->defaultId($scope) === $id];
$book = $book->without($scope, $id);
}
if ($errors !== []) {
return Errors::invalidInput('Ungültige Angaben; nichts wurde gelöscht.', $errors);
}
$deleted = array_column($targets, 'id');
$nested = [];
foreach ($targets as $target) {
foreach ($before->nestedUsers($target['id']) as $user) {
if (! in_array($user['id'], $deleted, true)) {
$nested[] = ['type' => 'preset', 'preset_id' => $user['id'], 'name' => $user['name'],
'scope' => $user['scope']->name];
}
}
}
$report = $this->usages->reportFor(new PresetUsageQuery($targets), false);
if ($nested !== [] || $report->exists()) {
return Errors::inUse(
'Die Presets werden noch verwendet; nichts wurde gelöscht.',
array_merge($nested, $report->listing()['affected']),
self::DETACH_HINT
);
}
$diff = PresetDiff::between($before, $book);
return new DesignPlan($book->data(), $diff, PresetDiff::summary($diff), null, [], true);
}
}
@@ -12,6 +12,7 @@ use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DiviPresetGetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
use LIT\MCP\Integrations\Divi\Design\PresetComposer;
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
@@ -166,6 +167,7 @@ final class DiviIntegration implements Integration
$loader,
$this->presetComposer(...)
),
new DiviPresetsDeleteAbility($this->designWriter, $this->designChange(), $this->globalUsages()),
];
}