feat: lit/divi-preset-get – Preset mit Einstellungen und Fundstellen lesen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
cc20100cbc
commit
341782a34f
@@ -0,0 +1,169 @@
|
||||
<?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\Abilities\JsonMap;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* lit/divi-preset-get (spec §6.2): one module or option group preset with its settings – short form where the
|
||||
* module's short form reaches them, the full form as attribute path => value like lit/divi-node-get –, its nested
|
||||
* group presets with names, the posts that use it (readable ones by title, the rest as a count) and the design_rev
|
||||
* for a following lit/divi-presets-set.
|
||||
*/
|
||||
final class DiviPresetGetAbility implements Ability
|
||||
{
|
||||
public function __construct(private DesignSystemWriter $writer, private GlobalUsages $usages)
|
||||
{
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'lit/divi-preset-get';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'Divi-Preset lesen';
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return 'Liest ein Modul-Preset (module, z. B. "button") oder Gruppen-Preset (group, z. B. "font-body") mit '
|
||||
. 'seinen Einstellungen (set in Kurzform, attrs als Attributpfad → Wert), verschachtelten Gruppen-Presets, '
|
||||
. 'den Seiten, die es verwenden (usage), und der design_rev für lit/divi-presets-set. preset = Name oder '
|
||||
. 'ID aus lit/divi-design-system (direkt: lit-divi-design-system).';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'module' => ['type' => 'string', 'description' => 'Modul, z. B. "button" (Modul-Preset).'],
|
||||
'group' => ['type' => 'string', 'description' => 'Gruppe, z. B. "font-body" (Gruppen-Preset).'],
|
||||
'preset' => ['type' => 'string', 'description' => 'Name oder ID des Presets.'],
|
||||
],
|
||||
'required' => ['preset'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(): ?array
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'kind' => ['type' => 'string'],
|
||||
'scope' => ['type' => 'string'],
|
||||
'default' => ['type' => 'boolean'],
|
||||
'module_name' => ['type' => 'string'],
|
||||
'group_id' => ['type' => ['string', 'null']],
|
||||
'set' => ['type' => 'object'],
|
||||
'attrs' => ['type' => 'object'],
|
||||
'group_presets' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||
'usage' => ['type' => 'object'],
|
||||
'design_rev' => ['type' => 'string'],
|
||||
],
|
||||
'required' => ['id', 'name', 'kind', 'scope', 'default', 'module_name', 'group_id', 'set', 'attrs',
|
||||
'group_presets', 'usage', 'design_rev'],
|
||||
];
|
||||
}
|
||||
|
||||
public function annotations(): array
|
||||
{
|
||||
return Annotations::read();
|
||||
}
|
||||
|
||||
public function isDirect(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function permission(array $input): bool|WP_Error
|
||||
{
|
||||
return current_user_can('edit_posts')
|
||||
? true
|
||||
: Errors::forbidden('Presets lesen erfordert das Recht, Beiträge zu bearbeiten.');
|
||||
}
|
||||
|
||||
public function execute(array $input): mixed
|
||||
{
|
||||
try {
|
||||
$scope = PresetScope::fromInput($input, 'input');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return Errors::invalidInput('Modul oder Gruppe fehlt bzw. ist ungültig.', ['module' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$book = PresetBook::fromDivi($this->writer->presets());
|
||||
$wanted = is_string($input['preset'] ?? null) ? $input['preset'] : '';
|
||||
try {
|
||||
$item = $book->find($scope, $wanted);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return Errors::invalidInput('Der Preset-Name ist mehrdeutig.', ['preset' => $e->getMessage()]);
|
||||
}
|
||||
if ($item === null) {
|
||||
return Errors::notFound(
|
||||
sprintf('%s „%s“ gibt es nicht.', $scope->label(), $wanted),
|
||||
'Vorhandene Presets mit lit/divi-design-system (direkt: lit-divi-design-system) und module auflisten.'
|
||||
);
|
||||
}
|
||||
|
||||
$id = (string) $item['id'];
|
||||
$isDefault = $book->defaultId($scope) === $id;
|
||||
$attrs = is_array($item['attrs'] ?? null) ? $item['attrs'] : [];
|
||||
$module = is_string($item['moduleName'] ?? null) ? $item['moduleName'] : $scope->name;
|
||||
$usage = $this->usages->reportFor(
|
||||
new PresetUsageQuery([['scope' => $scope, 'id' => $id, 'default' => $isDefault]]),
|
||||
false
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'name' => (string) ($item['name'] ?? $id),
|
||||
'kind' => $scope->kind,
|
||||
'scope' => $scope->name,
|
||||
'default' => $isDefault,
|
||||
'module_name' => $module,
|
||||
'group_id' => is_string($item['groupId'] ?? null) ? $item['groupId'] : null,
|
||||
'set' => JsonMap::of(ShortformMap::values(substr($module, (int) strpos($module, '/') + 1), $attrs)),
|
||||
'attrs' => JsonMap::of(AttrMap::flatten($attrs)),
|
||||
'group_presets' => $this->nested($book, $item),
|
||||
'usage' => $usage->listing(),
|
||||
'design_rev' => $book->rev(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $item
|
||||
* @return list<array{group_id: string, group_name: string, presets: list<array{id: string, name: string}>}>
|
||||
*/
|
||||
private function nested(PresetBook $book, array $item): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach (is_array($item['groupPresets'] ?? null) ? $item['groupPresets'] : [] as $groupId => $nested) {
|
||||
$groupName = is_array($nested) && is_string($nested['groupName'] ?? null) ? $nested['groupName'] : '';
|
||||
$presets = [];
|
||||
foreach (is_array($nested) ? (array) ($nested['presetId'] ?? []) : [] as $presetId) {
|
||||
$found = $book->items(new PresetScope(PresetScope::GROUP, $groupName))[(string) $presetId] ?? null;
|
||||
$presets[] = ['id' => (string) $presetId, 'name' => (string) ($found['name'] ?? $presetId)];
|
||||
}
|
||||
$rows[] = ['group_id' => (string) $groupId, 'group_name' => $groupName, 'presets' => $presets];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ use Closure;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
|
||||
use LIT\MCP\Integrations\Divi\Design\DiviPresetGetAbility;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
|
||||
@@ -57,6 +59,7 @@ final class DiviIntegration implements Integration
|
||||
private DesignSystemReader $design;
|
||||
private LibraryAccess $access;
|
||||
private ThemeBuilderAccess $themeBuilderAccess;
|
||||
private DesignSystemWriter $designWriter;
|
||||
|
||||
/**
|
||||
* @param (Closure(): bool)|null $libraryWritable Tools-tab gate for writes to library entries
|
||||
@@ -84,6 +87,7 @@ final class DiviIntegration implements Integration
|
||||
) {
|
||||
$this->modules = new DiviModules();
|
||||
$this->design = new DesignSystemReader();
|
||||
$this->designWriter = new DesignSystemWriter();
|
||||
$roles = new DiviRoles();
|
||||
$this->access = new LibraryAccess(new DiviLibraryStore(), $roles, $libraryWritable, $libraryVisible);
|
||||
$this->themeBuilderAccess = new ThemeBuilderAccess(
|
||||
@@ -150,6 +154,7 @@ final class DiviIntegration implements Integration
|
||||
new DiviReplaceTextAbility($this->schema, $loader, $writer),
|
||||
new DiviNodeCopyAbility($writer, $loader, $this->diviWritable, $this->libraryInsertable),
|
||||
new DiviDynamicSourcesAbility($this->schema, new DynamicContentMap(), $this->access->roles()),
|
||||
new DiviPresetGetAbility($this->designWriter, $this->globalUsages()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\DiviPresetGetAbility;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class DiviPresetGetAbilityTest extends TestCase
|
||||
{
|
||||
private function ability(?GlobalUsages $usages = null): DiviPresetGetAbility
|
||||
{
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('presets')->andReturn(PresetBookTest::stored());
|
||||
$usages ??= Mockery::mock(GlobalUsages::class);
|
||||
|
||||
return new DiviPresetGetAbility($writer, $usages);
|
||||
}
|
||||
|
||||
public function testReadsAPresetWithItsUsage(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$usages = Mockery::mock(GlobalUsages::class);
|
||||
$usages->expects('reportFor')
|
||||
->with(Mockery::type(PresetUsageQuery::class), false)
|
||||
->andReturn(new UsageReport(1, [['post_id' => 3, 'title' => 'Start', 'status' => 'publish',
|
||||
'type' => 'page']], 0, 1, [], 0, false));
|
||||
$ability = $this->ability($usages);
|
||||
|
||||
self::assertTrue($ability->permission(['module' => 'text', 'preset' => 'Fließtext']));
|
||||
$result = $ability->execute(['module' => 'text', 'preset' => 'Fließtext']);
|
||||
|
||||
self::assertSame(['dddddddddd', 'Fließtext', 'module', 'divi/text', true], [$result['id'], $result['name'],
|
||||
$result['kind'], $result['scope'], $result['default']]);
|
||||
self::assertSame([[
|
||||
'group_id' => 'content.decoration.bodyFont',
|
||||
'group_name' => 'divi/font-body',
|
||||
'presets' => [['id' => 'gggggggggg', 'name' => 'Lesetext']],
|
||||
]], $result['group_presets']);
|
||||
self::assertSame(3, $result['usage']['affected'][0]['post_id']);
|
||||
self::assertSame(PresetBook::fromDivi(PresetBookTest::stored())->rev(), $result['design_rev']);
|
||||
self::assertFalse($ability->isDirect());
|
||||
self::assertTrue($ability->annotations()['readonly']);
|
||||
}
|
||||
|
||||
public function testFlattensAttrsLikeNodeGet(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$usages = Mockery::mock(GlobalUsages::class);
|
||||
$usages->allows('reportFor')->andReturn(new UsageReport(0, [], 0, 0, [], 0, false));
|
||||
|
||||
$result = $this->ability($usages)->execute(['module' => 'button', 'preset' => 'aaaaaaaaaa']);
|
||||
|
||||
self::assertSame(['button.x' => 1], (array) $result['attrs']);
|
||||
}
|
||||
|
||||
public function testUnknownAndAmbiguousPresets(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
|
||||
self::assertToolError($this->ability()->execute(['module' => 'button', 'preset' => 'Fehlt']), 'not_found');
|
||||
$ambiguous = self::assertToolError($this->ability()->execute(['module' => 'button', 'preset' => 'Doppelt']),
|
||||
'validation_failed');
|
||||
self::assertStringContainsString('bbbbbbbbbb, cccccccccc', $ambiguous['fields']['preset']);
|
||||
self::assertToolError($this->ability()->execute(['preset' => 'x']), 'validation_failed');
|
||||
}
|
||||
|
||||
public function testNeedsEditPosts(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
self::assertToolError($this->ability()->permission([]), 'forbidden');
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ final class DiviIntegrationTest extends TestCase
|
||||
'lit/divi-replace-text',
|
||||
'lit/divi-node-copy',
|
||||
'lit/divi-dynamic-sources',
|
||||
'lit/divi-preset-get',
|
||||
];
|
||||
|
||||
public function testProvidesTheDiviAbilitiesOnlyWithDivi5(): void
|
||||
|
||||
Reference in New Issue
Block a user