From 56f435298d064ab3dbc41b6261867666758e78ab Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:39:15 +0200 Subject: [PATCH] BUGFIX: Nested content not editable after copy (9.0 port) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upmerges the fix https://github.com/neos/neos-ui/pull/3994 Reverts the removal of recursive() from https://github.com/neos/neos-ui/pull/3992 again Introduces new e2e test to ensure the funny php code works too - Edit new text node inline in container - Copy container with new text node - Edit copied text node inline ✓ Create a text node in a new container element at the correct position --- .../Changes/AbstractStructuralChange.php | 6 +++ .../Feedback/Operations/UpdateNodeInfo.php | 28 ++++++++++++-- .../createNewNodeInContainer.e2e.js | 38 ++++++++++++++++--- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/Classes/Domain/Model/Changes/AbstractStructuralChange.php b/Classes/Domain/Model/Changes/AbstractStructuralChange.php index 90aefd5064..71e99a521e 100644 --- a/Classes/Domain/Model/Changes/AbstractStructuralChange.php +++ b/Classes/Domain/Model/Changes/AbstractStructuralChange.php @@ -154,6 +154,12 @@ protected function finish(Node $node) $this->getParentDomAddress()->getContextPath() === NodeAddress::fromNode($parentNode)->toJson() ) { + // Include nested node information which is in the case of copying not available in the ui as only the root copied element is serialized to the ui. + $updateNestedNodeInfo = new UpdateNodeInfo(); + $updateNestedNodeInfo->setNode($node); + $updateNestedNodeInfo->recursive(); + $this->feedbackCollection->add($updateNestedNodeInfo); + $renderContentOutOfBand = new RenderContentOutOfBand(); $renderContentOutOfBand->setNode($node); $renderContentOutOfBand->setParentDomAddress($this->getParentDomAddress()); diff --git a/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php b/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php index 4509e63410..b3dd13c66d 100644 --- a/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php +++ b/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php @@ -41,6 +41,8 @@ class UpdateNodeInfo extends AbstractFeedback */ protected $contentRepositoryRegistry; + protected bool $isRecursive = false; + protected ?string $baseNodeType = null; public function setBaseNodeType(?string $baseNodeType): void @@ -58,6 +60,14 @@ public function setNode(Node $node): void $this->node = $node; } + /** + * Update node infos recursively + */ + public function recursive(): void + { + $this->isRecursive = true; + } + public function getNode(): Node { return $this->node; @@ -93,7 +103,7 @@ public function isSimilarTo(FeedbackInterface $feedback): bool public function serializePayload(ControllerContext $controllerContext): array { return [ - 'byContextPath' => $this->serializeNode($this->node, $controllerContext->getRequest()) + 'byContextPath' => $this->serializeNodeRecursively($this->node, $controllerContext->getRequest()) ]; } @@ -102,13 +112,23 @@ public function serializePayload(ControllerContext $controllerContext): array * * @return array> */ - private function serializeNode(Node $node, ActionRequest $actionRequest): array + private function serializeNodeRecursively(Node $node, ActionRequest $actionRequest): array { - return [ - NodeAddress::fromNode($node)->toJson() => $this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation( + $result = [ + NodeAddress::fromNode($node)->toJson() + => $this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation( $node, $actionRequest ) ]; + + if ($this->isRecursive === true) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + foreach ($subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create()) as $childNode) { + $result = array_merge($result, $this->serializeNodeRecursively($childNode, $actionRequest)); + } + } + + return $result; } } diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodeInContainer.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodeInContainer.e2e.js index f83f00fda1..6c21e0dfe7 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodeInContainer.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodeInContainer.e2e.js @@ -1,6 +1,6 @@ import {Selector} from 'testcafe'; import {ReactSelector} from 'testcafe-react-selectors'; -import {beforeEach, subSection, checkPropTypes} from './../../utils.js'; +import {beforeEach, subSection, checkPropTypes, typeTextInline} from './../../utils.js'; import {Page} from './../../pageModel'; /* global fixture:true */ @@ -10,26 +10,52 @@ fixture`Create new nodes` .afterEach(() => checkPropTypes()); test('Create a text node in a new container element at the correct position', async t => { - await t.switchToMainWindow(); + await t.click(Selector('#neos-ContentTree-ToggleContentTree')); subSection('Create content collection node'); await t - .click(Selector('#neos-ContentTree-ToggleContentTree')) .click(Page.treeNode.withText('Content Collection (main)')) .click(Selector('#neos-ContentTree-AddNode')) - .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Container_Test')); + .click(ReactSelector('NodeTypeItem').find('button').withExactText('Container_Test')); await Page.waitForIframeLoading(t); subSection('Create text node in container'); await t - .click(Page.treeNode.withText('Container')) + .click(Page.treeNode.withExactText('Container_Test')) .click(Selector('#neos-ContentTree-AddNode')) .click(Selector('#into')) - .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Text_Test')); + .click(ReactSelector('NodeTypeItem').find('button').withExactText('Text_Test')); await Page.waitForIframeLoading(t); + await t.expect(Page.treeNode.withExactText('Text_Test').exists).ok(); + await t.switchToIframe('[name="neos-content-main"]'); + await t.expect(Selector('.test-text').count).eql(1) const textIsInWrap = Selector('.test-container .test-text').parent().hasClass('test-container__inner-wrap'); await t.expect(textIsInWrap).ok(); + await t.switchToMainWindow(); + + subSection('Edit new text node inline in container'); + await typeTextInline(t, '.test-text [contenteditable="true"]', 'my text', 'p'); + await t.expect(Selector('.test-text').innerText).eql('my text') + await t.switchToMainWindow(); + + subSection('Copy container with new text node'); + await t + .click(Page.treeNode.withExactText('Container_Test')) + .click(Selector('#neos-ContentTree-CopySelectedNode')) + .click(Selector('#neos-ContentTree-PasteClipBoardNode')) + .click(Selector('#neos-InsertModeDialog button#after')) + .click(Selector('#neos-InsertModeModal-apply')) + await Page.waitForIframeLoading(t); + + await t.expect(Page.treeNode.withExactText('Container_Test').count).eql(2); + await t.expect(Page.treeNode.withExactText('Text_Test').count).eql(2); + + subSection('Edit copied text node inline'); + await typeTextInline(t, '.test-container:last-child .test-text [contenteditable="true"]', 'my copied text', 'p'); + await t.expect(Selector('.test-container:last-child .test-text').innerText).eql('my copied text'); + await t.expect(Selector('.test-container .test-text').innerText).eql('my text'); + await t.switchToMainWindow(); });