feat: lit/library-make-global – node to global module, all or nothing (E10)
- NewLibraryEntry: spec and creation of a new library entry, shared by lit/library-save and lit/library-make-global (NewDraft, P7, StoredNodeGuard) - UsageMatcher::referencesAny() (ruling Q7): a global entry never names any entry in any spelling Divi renders (P23/P24 forms, Divi 4 shortcodes inside Divi 5 values, values casting to 0); used by library-save scope global and make-global - make-global: entry created, node replaced by GlobalInstance::of, page through DiviWriter::commit; refused or rolled-back page deletes the entry, a partial page write keeps it (the page may show it); sources are pages and posts only Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -12,19 +12,15 @@ use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviErrors;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
|
||||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
use WP_Term;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -34,8 +30,9 @@ defined('ABSPATH') || exit;
|
||||
* divi/placeholder frame (LibraryContent::wrap, line breaks between blocks unified like
|
||||
* LibraryContent::load), saved through DiviSaver::saveNew with SaveScope::LibraryItem (the one
|
||||
* content write, D7; no Divi flags). The node is checked by StoredNodeGuard (E6) because it moves
|
||||
* to another post. The source is never written – linking it to the entry is
|
||||
* lit/library-make-global (Etappe 3b). All or nothing through NewDraft (ruling P6): any failure
|
||||
* to another post; a global entry names no other entry in any spelling (ruling Q7). Spec and creation
|
||||
* are NewLibraryEntry, shared with lit/library-make-global. The source is never written – linking it
|
||||
* to the entry is lit/library-make-global. All or nothing through NewDraft (ruling P6): any failure
|
||||
* after the insert deletes the new entry, a failed delete is an honest partial_write.
|
||||
*/
|
||||
final class LibrarySaveAbility implements Ability
|
||||
@@ -44,16 +41,17 @@ final class LibrarySaveAbility implements Ability
|
||||
|
||||
private const SCOPES = ['normal', 'global'];
|
||||
|
||||
private const REASON_LAYOUT = 'Der Inhalt des Bibliothekseintrags konnte nicht gespeichert werden.';
|
||||
private NewLibraryEntry $entries;
|
||||
|
||||
public function __construct(
|
||||
private LibraryAccess $access,
|
||||
private DiviPageLoader $loader,
|
||||
private DiviWriter $writer,
|
||||
private DiviSaver $saver,
|
||||
DiviSaver $saver,
|
||||
private ChangeLog $log,
|
||||
private DiviModules $modules
|
||||
DiviModules $modules
|
||||
) {
|
||||
$this->entries = new NewLibraryEntry($access, $saver, $modules);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
@@ -158,43 +156,18 @@ final class LibrarySaveAbility implements Ability
|
||||
return DiviErrors::unknownNode($id, $page->post);
|
||||
}
|
||||
|
||||
$title = sanitize_text_field(is_string($input['title'] ?? null) ? $input['title'] : '');
|
||||
if ($title === '') {
|
||||
return Errors::invalidInput('Titel fehlt; nichts wurde angelegt.', ['title' => 'nicht leer']);
|
||||
}
|
||||
|
||||
$spec = LibraryItemSpec::forNode(
|
||||
$spec = $this->entries->spec(
|
||||
$page,
|
||||
$node,
|
||||
$this->modules->catalog(),
|
||||
$title,
|
||||
$input,
|
||||
self::isGlobal($input),
|
||||
$this->builtFor($page)
|
||||
$this->writer->composition($page)->storedGuard()
|
||||
);
|
||||
if (is_string($spec)) {
|
||||
return Errors::invalidInput($spec . ' Nichts wurde angelegt.', ['id' => $spec]);
|
||||
if ($spec instanceof WP_Error) {
|
||||
return $spec;
|
||||
}
|
||||
|
||||
$problems = $this->writer->composition($page)->storedGuard()->problems($node);
|
||||
if ($problems !== []) {
|
||||
return Errors::invalidInput(
|
||||
'Der Knoten enthält Einstellungen, die nur im Divi-Builder übertragen werden dürfen; nichts wurde '
|
||||
. 'angelegt.',
|
||||
['id' => implode('; ', $problems)]
|
||||
);
|
||||
}
|
||||
|
||||
$categories = $this->termIds('layout_category', $input['categories'] ?? []);
|
||||
$tags = $this->termIds('layout_tag', $input['tags'] ?? []);
|
||||
foreach (['categories' => $categories, 'tags' => $tags] as $field => $ids) {
|
||||
if (is_string($ids)) {
|
||||
return Errors::invalidInput(
|
||||
'Unbekannte Kategorie oder unbekanntes Schlagwort; nichts wurde angelegt.',
|
||||
[$field => $ids]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->create($page, $spec->withTerms($categories, $tags), self::content($node), $input);
|
||||
return $this->create($page, $spec, $node, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,20 +175,19 @@ final class LibrarySaveAbility implements Ability
|
||||
* @return array<string, mixed>
|
||||
* @throws WriteFailed
|
||||
*/
|
||||
private function create(DiviPage $page, LibraryItemSpec $spec, string $content, array $input): array
|
||||
private function create(DiviPage $page, LibraryItemSpec $spec, Block $node, array $input): array
|
||||
{
|
||||
$draft = new NewDraft(null, $this->log, $this->name(), DiviLibraryStore::POST_TYPE, 'Bibliothekseintrag');
|
||||
$libraryId = $this->access->store()->create($spec, $draft);
|
||||
$new = $this->entries->create($spec, $node, $draft);
|
||||
$libraryId = $new['id'];
|
||||
|
||||
$warnings = [];
|
||||
try {
|
||||
$warnings = $this->saver->saveNew($libraryId, $content, SaveScope::LibraryItem);
|
||||
} catch (Throwable $e) {
|
||||
$draft->discard($libraryId, $e, self::REASON_LAYOUT);
|
||||
}
|
||||
|
||||
$followUp = (new PostSaveFollowUp($this->name(), $this->log, $libraryId, 'der Bibliothekseintrag', $warnings))
|
||||
->afterSave($input);
|
||||
$followUp = (new PostSaveFollowUp(
|
||||
$this->name(),
|
||||
$this->log,
|
||||
$libraryId,
|
||||
'der Bibliothekseintrag',
|
||||
$new['warnings']
|
||||
))->afterSave($input);
|
||||
$created = $followUp->reread();
|
||||
$kind = (self::TYPE_LABELS[$spec->layoutType] ?? $spec->layoutType) . ($spec->global ? ', global' : '');
|
||||
$followUp->log(
|
||||
@@ -223,9 +195,10 @@ final class LibrarySaveAbility implements Ability
|
||||
sprintf('Bibliothek: „%s“ angelegt (%s) aus Eintrag %d', $spec->title, $kind, (int) $page->post->ID)
|
||||
);
|
||||
|
||||
$content = $created !== null ? (string) $created->post_content : NewLibraryEntry::content($node);
|
||||
$result = $followUp->result(sprintf('Bibliothekseintrag „%s“ angelegt (%s).', $spec->title, $kind), $created)
|
||||
->with('library_id', $libraryId)
|
||||
->with('tree_rev', Rev::of($created !== null ? (string) $created->post_content : $content))
|
||||
->with('tree_rev', Rev::of($content))
|
||||
->urls(null, (string) get_edit_post_link($libraryId, 'raw'));
|
||||
if ($spec->global) {
|
||||
$result->warning(
|
||||
@@ -237,48 +210,9 @@ final class LibrarySaveAbility implements Ability
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
/** The node's stored bytes framed like the builder's "add to library", line breaks unified. */
|
||||
private static function content(Block $node): string
|
||||
{
|
||||
$copy = $node->copy();
|
||||
$copy->unifyLineBreaks();
|
||||
|
||||
return LibraryContent::wrap($copy);
|
||||
}
|
||||
|
||||
/** _et_pb_built_for_post_type: the source's post type; a library entry passes its own on. */
|
||||
private function builtFor(DiviPage $page): string
|
||||
{
|
||||
if (! $page->isLibraryItem()) {
|
||||
return (string) $page->post->post_type;
|
||||
}
|
||||
|
||||
$builtFor = (string) get_post_meta((int) $page->post->ID, '_et_pb_built_for_post_type', true);
|
||||
|
||||
return $builtFor !== '' ? $builtFor : 'page';
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $input */
|
||||
private static function isGlobal(array $input): bool
|
||||
{
|
||||
return ($input['scope'] ?? 'normal') === 'global';
|
||||
}
|
||||
|
||||
/** @return list<int>|string Term IDs of existing terms by name, or the problem. */
|
||||
private function termIds(string $taxonomy, mixed $names): array|string
|
||||
{
|
||||
$ids = [];
|
||||
foreach (is_array($names) ? $names : [] as $name) {
|
||||
$term = is_string($name) ? get_term_by('name', $name, $taxonomy) : false;
|
||||
if (! $term instanceof WP_Term) {
|
||||
return sprintf(
|
||||
'„%s“ gibt es nicht; nur vorhandene Begriffe (neue in der Divi-Bibliothek anlegen).',
|
||||
is_string($name) ? $name : '?'
|
||||
);
|
||||
}
|
||||
$ids[] = (int) $term->term_id;
|
||||
}
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user