diff --git a/src/Integrations/Divi/Pages/DiviDuplicateNodeAbility.php b/src/Integrations/Divi/Pages/DiviDuplicateNodeAbility.php index 02d9d90..88aa8cc 100644 --- a/src/Integrations/Divi/Pages/DiviDuplicateNodeAbility.php +++ b/src/Integrations/Divi/Pages/DiviDuplicateNodeAbility.php @@ -7,7 +7,6 @@ namespace LIT\MCP\Integrations\Divi\Pages; use LIT\MCP\Abilities\Ability; use LIT\MCP\Abilities\ToolResult; use LIT\MCP\Integrations\Divi\BlockTree\AttrMap; -use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary; use WP_Error; /** @@ -86,10 +85,16 @@ final class DiviDuplicateNodeAbility implements Ability $copy = $this->writer->composition($page)->editor()->duplicate($page->index, $node); if ($copy->isDivi() && ! $copy->isOpaque()) { - $label = NodeSummary::label($node); - $copy->setAttrs(AttrMap::set($copy->attrs(), 'module.meta.adminLabel', [ - 'desktop' => ['value' => ($label === '' ? $node->shortName() : $label) . ' (Kopie)'], - ])); + // The raw stored desktop value (never NodeSummary::label()'s display-shortened + // text): a long or multi-line label must be copied verbatim, only " (Kopie)" added. + // Setting only the ".desktop.value" leaf keeps every other breakpoint untouched. + $raw = AttrMap::desktop($node->attrs(), 'module.meta.adminLabel'); + $label = is_string($raw) ? $raw : ''; + $copy->setAttrs(AttrMap::set( + $copy->attrs(), + 'module.meta.adminLabel.desktop.value', + ($label === '' ? $node->shortName() : $label) . ' (Kopie)' + )); } $change = (new DiviChange(sprintf('Knoten %s dupliziert', $id)))->changed($copy, 'duplicated'); diff --git a/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php b/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php index ca60114..9aa40ca 100644 --- a/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php +++ b/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php @@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages; use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; use LIT\MCP\ChangeLog\ChangeLog; +use LIT\MCP\Integrations\Divi\BlockTree\AttrMap; use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex; use LIT\MCP\Integrations\Divi\BlockTree\Parser; use LIT\MCP\Integrations\Divi\Design\DesignSystemReader; @@ -458,6 +459,60 @@ final class DiviWriteAbilitiesTest extends TestCase self::assertSame(str_replace($globalRow, $globalRow . $globalRow, $mixed), $this->saved[0]); } + /** + * Fix round 1 (Important-1): the copy's admin label must be the raw stored desktop value + * plus " (Kopie)" – never NodeSummary::label()'s display-shortened text (80-char limit). + */ + public function testDuplicateCopiesALongAdminLabelVerbatim(): void + { + $long = str_repeat('Ausführliche Beschriftung, ', 4) . 'Ende'; + self::assertGreaterThan(80, strlen($long), 'precondition: longer than the NodeSummary::label() display limit'); + $content = str_replace( + '"module":{"meta":{"adminLabel":{"desktop":{"value":"Hero – Button"}}}}', + '"module":{"meta":{"adminLabel":{"desktop":{"value":"' . $long . '"}}}}', + $this->original + ); + self::assertNotSame($content, $this->original, 'precondition: replaced exactly once'); + $this->post->post_content = $content; + + $result = $this->duplicate()->execute(['post_id' => 7, 'tree_rev' => Rev::of($content), 'id' => 's0.r0.c1.m2']); + + self::assertTrue($result['ok'], (string) json_encode($result)); + $index = NodeIndex::build((new Parser())->parse($this->saved[0])); + $copy = $index->find('s0.r0.c1.m3'); + self::assertNotNull($copy, 'the duplicate sits right after the original module'); + self::assertSame( + $long . ' (Kopie)', + AttrMap::desktop($copy->attrs(), 'module.meta.adminLabel'), + 'stored verbatim, not shortened/ellipsised for display' + ); + } + + public function testDuplicateCopiesALabelWithNewlinesAndMultipleSpacesVerbatim(): void + { + $messy = "Erste Zeile mit viel Leerraum\nZweite Zeile"; + $encoded = substr((string) json_encode($messy, JSON_UNESCAPED_UNICODE), 1, -1); + $content = str_replace( + '"module":{"meta":{"adminLabel":{"desktop":{"value":"Hero – Button"}}}}', + '"module":{"meta":{"adminLabel":{"desktop":{"value":"' . $encoded . '"}}}}', + $this->original + ); + self::assertNotSame($content, $this->original, 'precondition: replaced exactly once'); + $this->post->post_content = $content; + + $result = $this->duplicate()->execute(['post_id' => 7, 'tree_rev' => Rev::of($content), 'id' => 's0.r0.c1.m2']); + + self::assertTrue($result['ok'], (string) json_encode($result)); + $index = NodeIndex::build((new Parser())->parse($this->saved[0])); + $copy = $index->find('s0.r0.c1.m3'); + self::assertNotNull($copy, 'the duplicate sits right after the original module'); + self::assertSame( + $messy . ' (Kopie)', + AttrMap::desktop($copy->attrs(), 'module.meta.adminLabel'), + 'whitespace/newlines kept verbatim, not collapsed' + ); + } + /** Permission denied (edit_post false) for all three new abilities. */ public function testPermissionDeniedWhenNotEditable(): void {