feat: Divi tools edit Theme Builder layouts; visible layouts need confirm

TB layouts load through ThemeBuilderAccess (P12), live layouts (attached +
enabled, Q8) need a dry run and confirm (Q9) and get the shared restore point
of VisibleEdits, extracted from GlobalEdits (Q2: backup when revisions are
off, P9 refusals, P14 naming). divi/post-content only in body layouts (F22),
also for nested stored nodes. SaveScope::ThemeBuilderLayout sets Divi flags
and clears site CSS + TB cache.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 15:45:07 +02:00
co-authored by Claude Opus 5.5
parent 4ff1c1e2a5
commit b1dd35abd8
27 changed files with 1106 additions and 132 deletions
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
use LIT\MCP\Integrations\Divi\Pages\VisibleEdits;
use WP_Error;
defined('ABSPATH') || exit;
/**
* The extra rule of a Divi write to a Theme Builder layout (Etappe 3b, constraint "sichtbare Website nur mit
* ausdrücklicher Freigabe"): Divi reads TB layouts live (F22), so a layout that a live template (attached to the
* live Theme Builder and enabled, whatever its post status – ruling Q8) shows changes the website at once –
* without confirm: true nothing is saved and the answer lists those templates (dry run of the fully validated
* change, like global modules, answer F2; ruling Q9). A confirmed edit gets the restore point of every visible
* change (VisibleEdits, ruling Q2): a backup when revisions are off, refused when it could not be faithful (P9).
* Layouts of disabled or detached templates save at once. The save scope sets the Divi flags and clears every
* page's CSS and the Theme Builder cache.
*/
final class ThemeBuilderEdits
{
public function __construct(
private ThemeBuilderAccess $access,
private VisibleEdits $visible = new VisibleEdits()
) {
}
/** @return list<Template> Live templates that show layout $page in its area. */
public function showing(DiviPage $page): array
{
$area = (string) $page->themeBuilderArea();
$id = (int) $page->post->ID;
return array_values(array_filter(
$this->access->store()->liveTemplates(),
static fn (Template $template): bool => $template->showsLayout($area, $id)
));
}
/**
* @param array<string, mixed> $input
* @return array{scope: SaveScope, details: array<string, mixed>}|WP_Error
* @throws WriteFailed When the backup could not be made (nothing was saved).
*/
public function prepare(DiviPage $page, array $input): array|WP_Error
{
$templates = $this->showing($page);
$details = ['templates' => self::listing($templates)];
if ($templates === []) {
return ['scope' => SaveScope::ThemeBuilderLayout, 'details' => $details];
}
$id = (int) $page->post->ID;
if (($input['confirm'] ?? false) !== true) {
return Errors::confirmRequired(
sprintf(
'Theme-Builder-Layout %d ist auf der Website sichtbar (%s, siehe templates); die Änderung wirkt '
. 'sofort. Nichts wurde gespeichert (Probelauf).',
$id,
self::count($templates)
),
$details
);
}
$refusal = $this->visible->backupRefusal(
$page->post,
sprintf('Theme-Builder-Layout %d', $id),
'Revisionen aktivieren (WP_POST_REVISIONS) oder das Layout im Divi Theme Builder bearbeiten.'
);
if ($refusal !== null) {
return $refusal;
}
return [
'scope' => SaveScope::ThemeBuilderLayout,
'details' => array_merge($details, $this->visible->backup($page->post)),
];
}
/** A tool's own confirm_required (lit-divi-delete) on a visible layout, extended by the templates. */
public function notice(DiviPage $page, WP_Error $error): WP_Error
{
$templates = Errors::codeOf($error) === 'confirm_required' ? $this->showing($page) : [];
if ($templates === []) {
return $error;
}
return VisibleEdits::merged(
$error,
sprintf('Das Layout ist auf der Website sichtbar (%s, siehe templates).', self::count($templates)),
['templates' => self::listing($templates)]
);
}
/** @param list<Template> $templates */
private static function count(array $templates): string
{
return sprintf('%d aktive %s', count($templates), count($templates) === 1 ? 'Vorlage' : 'Vorlagen');
}
/**
* @param list<Template> $templates
* @return list<array{id: int, title: string, use_on: list<string>}>
*/
private static function listing(array $templates): array
{
return array_map(
static fn (Template $template): array => [
'id' => $template->id,
'title' => $template->title,
'use_on' => $template->useOn,
],
$templates
);
}
}