feat: lit/tb-list and the tool group "Divi Theme Builder"

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 15:57:45 +02:00
co-authored by Claude Opus 5.5
parent ac69fdf113
commit 54922a9f7e
6 changed files with 455 additions and 1 deletions
+12
View File
@@ -36,6 +36,7 @@ use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderIntegration;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
@@ -170,6 +171,17 @@ final class DiviIntegration implements Integration
);
}
/** The group "Divi Theme Builder" (lit/tb-*), sharing this integration's Theme Builder access (Etappe 3b). */
public function themeBuilder(): ThemeBuilderIntegration
{
return new ThemeBuilderIntegration(
$this->themeBuilderAccess,
new DiviComposition($this->modules, $this->design),
new DiviSaver(),
$this->log
);
}
private function writer(DiviPageLoader $loader): DiviWriter
{
return new DiviWriter(
@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\WordPress\PostGuard;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/tb-list (spec §5.7): the templates of the live Theme Builder with conditions (Divi's labels), layouts
* (title, shared with other templates – the "global" header/footer is the default template's layout shared by
* ID), template_rev for the writing TB tools; templates a later Theme Builder save removed ("detached", with
* Divi's unused mark – Divi trashes marked posts 7 days later, F19, stage3b finding 10) so they can be
* reattached; optionally Divi's condition catalog. Needs ThemeBuilderAccess::canUse(). Ruling Q8/B11: a
* template the user may not edit does not appear at all (not listed, not in shared_with, asked for by ID it
* reads like a missing one – P12); layout titles only for layouts the user may edit; condition labels name
* objects by the stored ID only, never by title.
*/
final class TbListAbility implements Ability
{
public const DETACHED_LIMIT = 50;
public function __construct(private ThemeBuilderAccess $access, private TemplateConditions $conditions)
{
}
public function name(): string
{
return 'lit/tb-list';
}
public function label(): string
{
return 'Theme-Builder-Vorlagen';
}
public function description(): string
{
return 'Listet die Vorlagen des Divi Theme Builders: aktiv oder deaktiviert, Bedingungen mit Beschriftung, '
. 'Layouts für Kopfzeile, Inhalt und Fußzeile (Layout-IDs lassen sich mit den Divi-Werkzeugen bearbeiten, '
. 'z. B. lit-divi-outline mit post_id) und template_rev für lit/tb-assign und lit/tb-activate. '
. 'template_id: nur diese Vorlage. conditions: true liefert alle Bedingungen, die Divi anbietet; IDs, die '
. 'auf „:“ enden, brauchen ein Objekt (z. B. "singular:post_type:page:id:42"). detached: Vorlagen, die ein '
. 'späteres Speichern im Theme Builder entfernt hat; nur auf ausdrücklichen Wunsch wieder zuordnen.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'template_id' => ['type' => 'integer', 'minimum' => 1],
'conditions' => ['type' => 'boolean', 'description' => 'Divis Bedingungen mitliefern.'],
],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
$objects = ['type' => 'array', 'items' => ['type' => 'object']];
return [
'type' => 'object',
'properties' => [
'theme_builder_id' => ['type' => 'integer'],
'draft_pending' => ['type' => 'boolean'],
'templates' => $objects,
'detached' => $objects,
'conditions' => $objects,
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
],
'required' => ['theme_builder_id', 'draft_pending', 'templates', 'detached', 'notes'],
];
}
public function annotations(): array
{
return Annotations::read();
}
public function isDirect(): bool
{
return false;
}
public function permission(array $input): bool|WP_Error
{
return $this->access->canUse() ?? true;
}
public function execute(array $input): mixed
{
$refusal = $this->access->canUse();
if ($refusal !== null) {
return $refusal;
}
$store = $this->access->store();
$editable = fn (Template $template): bool => $this->access->canEditTemplate($template->id);
$live = array_values(array_filter($store->liveTemplates(), $editable));
$detached = [];
foreach ($store->detachedIds(self::DETACHED_LIMIT) as $id) {
$template = $store->template($id, false);
if ($template !== null && $editable($template)) {
$detached[] = $template;
}
}
// Only what the user may see counts for shared_with (B11).
$shared = array_merge($live, $detached);
if (isset($input['template_id'])) {
$id = (int) PostGuard::id($input['template_id']);
$only = static fn (array $list): array => array_values(
array_filter($list, static fn (Template $template): bool => $template->id === $id)
);
$live = $only($live);
$detached = $only($detached);
if ($live === [] && $detached === []) {
return Errors::notFound(
sprintf('Theme-Builder-Vorlage %d gibt es nicht.', $id),
'Vorlagen mit lit/tb-list ohne template_id auflisten.'
);
}
}
$draft = $store->draftId() > 0;
$notes = [];
if ($draft) {
$notes[] = 'Der Theme Builder hat ungespeicherte Änderungen (Entwurf); bis sie dort gespeichert oder '
. 'verworfen sind, ändert LIT MCP keine Vorlagen.';
}
if ($detached !== []) {
$notes[] = 'detached: Diese Vorlagen hat ein späteres Speichern im Theme Builder entfernt – oder jemand '
. 'hat sie dort bewusst gelöscht; Divi löscht als ungenutzt markierte Vorlagen und Layouts 7 Tage '
. 'nach der Markierung. Nur auf ausdrücklichen Wunsch wieder zuordnen (bleibt deaktiviert): '
. 'lit/tb-assign mit reattach: true.';
}
$result = [
'theme_builder_id' => $store->liveId(),
'draft_pending' => $draft,
'templates' => array_map(fn (Template $t): array => $this->describe($t, $shared), $live),
'detached' => array_map(fn (Template $t): array => $this->describe($t, $shared), $detached),
'notes' => $notes,
];
if (($input['conditions'] ?? false) === true) {
$result['conditions'] = $this->conditions->available();
}
return $result;
}
/**
* @param list<Template> $visible Templates the user may see (the shared_with candidates).
* @return array<string, mixed>
*/
private function describe(Template $template, array $visible): array
{
$row = $template->toArray();
foreach (Template::AREAS as $area) {
$id = $template->layouts[$area]['id'];
$others = array_filter(
$visible,
static fn (Template $other): bool => $id > 0 && $other->id !== $template->id
&& $other->layouts[$area]['id'] === $id
);
$row['layouts'][$area]['title'] = $id > 0 && current_user_can('edit_post', $id)
? (string) get_the_title($id)
: '';
$row['layouts'][$area]['shared_with'] = array_values(array_map(
static fn (Template $other): int => $other->id,
$others
));
}
$row['use_on_labels'] = array_map($this->conditions->label(...), $template->useOn);
$row['exclude_from_labels'] = array_map($this->conditions->label(...), $template->excludeFrom);
return $row;
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Integration;
defined('ABSPATH') || exit;
/**
* The group "Divi Theme Builder" (Etappe 3b): lit/tb-list, lit/tb-create-template, lit/tb-assign and
* lit/tb-activate, all indirect. Built by DiviIntegration::themeBuilder() so it shares the Theme Builder access
* (rights, Tools-tab gates – its id ThemeBuilderAccess::GROUP is what the gates of the Divi tools ask; once this
* group is registered, its mode and "nur lesen" switch in the Tools tab drive them). Available with the Divi 5
* builder, like the Divi group; missing Divi internals show in lit-site-status. Composition, saver and log are
* for the writing tools of the later tasks.
*/
final class ThemeBuilderIntegration implements Integration
{
public function __construct(
private ThemeBuilderAccess $access,
private DiviComposition $composition,
private DiviSaver $saver,
private ChangeLog $log
) {
}
public function id(): string
{
return ThemeBuilderAccess::GROUP;
}
public function label(): string
{
return __('Divi Theme Builder', 'wp-lit-mcp');
}
public function isAvailable(): bool
{
return DiviModules::isAvailable();
}
public function abilities(): array
{
$conditions = new TemplateConditions($this->access->store());
return [
new TbListAbility($this->access, $conditions),
];
}
}
+2 -1
View File
@@ -123,7 +123,8 @@ final class Plugin
),
new MediaIntegration($schema, $changeLog, $limits),
$divi,
$divi->library()
$divi->library(),
$divi->themeBuilder()
);
$policy = ExposurePolicy::fromOption()->withGroups($registry->groupMap(...));