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:
@@ -14,6 +14,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
use WP_Error;
|
||||
|
||||
@@ -25,7 +26,8 @@ defined('ABSPATH') || exit;
|
||||
* tree – only when its whole input validated (all or nothing) –, serialize, save via DiviSaver,
|
||||
* then lit_mcp_after_save, change log and the ToolResult with changed nodes, affected outline,
|
||||
* quality warnings (spec §5.5), revision_id, the new rev and tree_rev. commit() is the entry point
|
||||
* of the tools: library entries additionally pass GlobalEdits (shape, dry run/confirm, backup).
|
||||
* of the tools: library entries additionally pass GlobalEdits (shape, dry run/confirm, backup), Theme
|
||||
* Builder layouts ThemeBuilderEdits (live layouts: dry run/confirm, backup; ruling Q2/Q9).
|
||||
*/
|
||||
final class DiviWriter
|
||||
{
|
||||
@@ -38,7 +40,8 @@ final class DiviWriter
|
||||
private ChangeLog $log,
|
||||
private DesignSystemReader $design,
|
||||
private DiviComposition $composition,
|
||||
private ?GlobalEdits $globals = null
|
||||
private ?GlobalEdits $globals = null,
|
||||
private ?ThemeBuilderEdits $themeBuilder = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -67,11 +70,12 @@ final class DiviWriter
|
||||
|
||||
/**
|
||||
* The Compose classes for $page: bound to its entry, so placeholders and $variable(…)$
|
||||
* references are checked against Divi's dynamic content options for it (ruling S22).
|
||||
* references are checked against Divi's dynamic content options for it (ruling S22); a Theme
|
||||
* Builder layout also by its area (F22: modules Divi renders only elsewhere are refused).
|
||||
*/
|
||||
public function composition(DiviPage $page): DiviComposition
|
||||
{
|
||||
return $this->composition->forPost((int) $page->post->ID);
|
||||
return $this->composition->forPost((int) $page->post->ID, $page->themeBuilderArea());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +155,7 @@ final class DiviWriter
|
||||
/**
|
||||
* save() for the Divi tools (E2): pages as before; a library entry keeps its shape, a global one
|
||||
* needs confirm (dry run first), gets a backup when revisions are off and clears every page's
|
||||
* cache; the result then names the affected pages.
|
||||
* cache; the result then names the affected pages. Theme Builder layouts: commitLayout().
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return array<string, mixed>|WP_Error
|
||||
@@ -159,6 +163,10 @@ final class DiviWriter
|
||||
*/
|
||||
public function commit(string $ability, DiviPage $page, DiviChange $change, array $input): array|WP_Error
|
||||
{
|
||||
if ($page->isThemeBuilderLayout()) {
|
||||
return $this->commitLayout($ability, $page, $change, $input);
|
||||
}
|
||||
|
||||
if (! $page->isLibraryItem()) {
|
||||
return $this->save($ability, $page, $change, $input);
|
||||
}
|
||||
@@ -179,48 +187,71 @@ final class DiviWriter
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
return $this->savePrepared($ability, $page, $change, $input, $prepared, 'des Bibliothekseintrags');
|
||||
}
|
||||
|
||||
/**
|
||||
* save() for a Theme Builder layout: a layout visitors see needs confirm and gets the restore point
|
||||
* of a visible change (ThemeBuilderEdits, rulings Q2/Q9); the result names the templates that show it.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @return array<string, mixed>|WP_Error
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
private function commitLayout(string $ability, DiviPage $page, DiviChange $change, array $input): array|WP_Error
|
||||
{
|
||||
if ($this->themeBuilder === null) {
|
||||
return Errors::unsupported(
|
||||
'Theme-Builder-Layouts lassen sich über diese Verbindung nicht ändern.',
|
||||
'Das Layout im Divi Theme Builder bearbeiten.'
|
||||
);
|
||||
}
|
||||
|
||||
if (Serializer::document($page->document) === (string) $page->post->post_content) {
|
||||
return $this->save($ability, $page, $change, $input, SaveScope::ThemeBuilderLayout);
|
||||
}
|
||||
|
||||
$prepared = $this->themeBuilder->prepare($page, $input);
|
||||
if ($prepared instanceof WP_Error) {
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
return $this->savePrepared($ability, $page, $change, $input, $prepared, 'des Theme-Builder-Layouts');
|
||||
}
|
||||
|
||||
/**
|
||||
* save() with the scope GlobalEdits/ThemeBuilderEdits prepared; their details join the result, and a
|
||||
* failure after their backup names it (ruling P14, VisibleEdits). $entry names the entry in that error.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
* @param array{scope: SaveScope, details: array<string, mixed>} $prepared
|
||||
* @return array<string, mixed>
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
private function savePrepared(
|
||||
string $ability,
|
||||
DiviPage $page,
|
||||
DiviChange $change,
|
||||
array $input,
|
||||
array $prepared,
|
||||
string $entry
|
||||
): array {
|
||||
try {
|
||||
$saved = $this->save($ability, $page, $change, $input, $prepared['scope']);
|
||||
} catch (WriteFailed $e) {
|
||||
throw self::keepingBackup($e, $prepared['details']['backup_id'] ?? null);
|
||||
throw VisibleEdits::keepingBackup($e, $prepared['details']['backup_id'] ?? null, $entry);
|
||||
}
|
||||
|
||||
return array_merge($saved, $prepared['details']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P14: GlobalEdits made a backup (revisions off) before the save failed – the error names
|
||||
* it, because it stays in the trash and is the only earlier state.
|
||||
*/
|
||||
private static function keepingBackup(WriteFailed $e, mixed $backupId): WriteFailed
|
||||
{
|
||||
if (! is_int($backupId)) {
|
||||
return $e;
|
||||
}
|
||||
|
||||
$kept = sprintf(
|
||||
'Sicherung %d liegt im Papierkorb (vorheriger Stand des Bibliothekseintrags, in WordPress '
|
||||
. 'wiederherstellbar).',
|
||||
$backupId
|
||||
);
|
||||
if ($e->rolledBack) {
|
||||
return $e->withDetails(
|
||||
['backup_id' => $backupId],
|
||||
$kept . ' Den Aufruf erst nach Prüfung mit lit-divi-outline wiederholen.',
|
||||
Errors::notSavedKeeping(sprintf('Sicherung %d im Papierkorb', $backupId))
|
||||
);
|
||||
}
|
||||
|
||||
return $e->withDetails(
|
||||
['backup_id' => $backupId],
|
||||
'Den Aufruf nicht einfach wiederholen. ' . $kept . ' Zuerst den Stand mit lit-divi-outline prüfen und '
|
||||
. 'dem Nutzer Bescheid geben.'
|
||||
);
|
||||
}
|
||||
|
||||
/** A tool's confirm_required on a global library entry, extended by the affected pages. */
|
||||
/** A tool's confirm_required on a global library entry or a live Theme Builder layout, extended by who sees it. */
|
||||
public function notice(DiviPage $page, WP_Error $error): WP_Error
|
||||
{
|
||||
if ($page->isThemeBuilderLayout()) {
|
||||
return $this->themeBuilder?->notice($page, $error) ?? $error;
|
||||
}
|
||||
|
||||
return $page->isLibraryItem() && $this->globals !== null ? $this->globals->notice($page, $error) : $error;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user