diff --git a/Classes/ContentRepository/Service/WorkspaceService.php b/Classes/ContentRepository/Service/WorkspaceService.php index 7a55e24b05..a0e5622235 100644 --- a/Classes/ContentRepository/Service/WorkspaceService.php +++ b/Classes/ContentRepository/Service/WorkspaceService.php @@ -13,7 +13,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; +use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardIndividualNodesFromWorkspace; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\Neos\FrontendRouting\NodeAddress; @@ -24,8 +24,8 @@ use Neos\Flow\Annotations as Flow; use Neos\Neos\Domain\Service\UserService as DomainUserService; use Neos\Neos\PendingChangesProjection\ChangeFinder; -use Neos\Neos\PendingChangesProjection\ChangeProjection; use Neos\Neos\Service\UserService; +use Neos\Neos\Ui\Domain\Model\Feedback\Operations\RemoveNode; /** * @Flow\Scope("singleton") @@ -156,6 +156,53 @@ public function getAllowedTargetWorkspaces(ContentRepository $contentRepository) return $workspacesArray; } + public function predictRemoveNodeFeedbackFromDiscardIndividualNodesFromWorkspaceCommand( + DiscardIndividualNodesFromWorkspace $command, + ContentRepository $contentRepository + ): array { + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->workspaceName); + if (is_null($workspace)) { + return Nodes::createEmpty(); + } + + $changeFinder = $contentRepository->projectionState(ChangeFinder::class); + $changes = $changeFinder->findByContentStreamId($workspace->currentContentStreamId); + + $handledNodes = []; + $result = []; + foreach ($changes as $change) { + if ($change->created) { + foreach ($command->nodesToDiscard as $nodeToDiscard) { + if (in_array($nodeToDiscard, $handledNodes)) { + continue; + } + + if ( + $nodeToDiscard->contentStreamId->equals($change->contentStreamId) + && $nodeToDiscard->nodeAggregateId->equals($change->nodeAggregateId) + && $nodeToDiscard->dimensionSpacePoint->equals($change->originDimensionSpacePoint) + ) { + $subgraph = $contentRepository->getContentGraph() + ->getSubgraph( + $nodeToDiscard->contentStreamId, + $nodeToDiscard->dimensionSpacePoint, + VisibilityConstraints::withoutRestrictions() + ); + + $childNode = $subgraph->findNodeById($nodeToDiscard->nodeAggregateId); + $parentNode = $subgraph->findParentNode($nodeToDiscard->nodeAggregateId); + if ($parentNode) { + $result[] = new RemoveNode($childNode, $parentNode); + $handledNodes[] = $nodeToDiscard; + } + } + } + } + } + + return $result; + } + private function getClosestDocumentNode(Node $node): ?Node { $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index 37b09703a7..a4e73cc69f 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -294,18 +294,27 @@ public function discardAction(array $nodeContextPaths): void $nodeAddress->dimensionSpacePoint ); } - $contentRepository->handle( - DiscardIndividualNodesFromWorkspace::create( - $workspaceName, - NodeIdsToPublishOrDiscard::create(...$nodeIdentifiersToDiscard) - ) - )->block(); + + $command = DiscardIndividualNodesFromWorkspace::create( + $workspaceName, + NodeIdsToPublishOrDiscard::create(...$nodeIdentifiersToDiscard) + ); + $removeNodeFeedback = $this->workspaceService + ->predictRemoveNodeFeedbackFromDiscardIndividualNodesFromWorkspaceCommand( + $command, + $contentRepository + ); + + $contentRepository->handle($command)->block(); $success = new Success(); $success->setMessage(sprintf('Discarded %d node(s).', count($nodeContextPaths))); $updateWorkspaceInfo = new UpdateWorkspaceInfo($contentRepositoryId, $workspaceName); $this->feedbackCollection->add($success); + foreach ($removeNodeFeedback as $removeNode) { + $this->feedbackCollection->add($removeNode); + } $this->feedbackCollection->add($updateWorkspaceInfo); } catch (\Exception $e) { $error = new Error(); diff --git a/Classes/Domain/Model/Feedback/Operations/RemoveNode.php b/Classes/Domain/Model/Feedback/Operations/RemoveNode.php index 389d35989b..f068c2ccba 100644 --- a/Classes/Domain/Model/Feedback/Operations/RemoveNode.php +++ b/Classes/Domain/Model/Feedback/Operations/RemoveNode.php @@ -14,6 +14,7 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Neos\FrontendRouting\NodeAddressFactory; +use Neos\Neos\FrontendRouting\NodeAddress; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; @@ -25,6 +26,10 @@ class RemoveNode extends AbstractFeedback protected Node $parentNode; + private NodeAddress $nodeAddress; + + private NodeAddress $parentNodeAddress; + /** * @Flow\Inject * @var ContentRepositoryRegistry @@ -37,6 +42,15 @@ public function __construct(Node $node, Node $parentNode) $this->parentNode = $parentNode; } + protected function initializeObject(): void + { + $contentRepository = $this->contentRepositoryRegistry->get($this->node->subgraphIdentity->contentRepositoryId); + $nodeAddressFactory = NodeAddressFactory::create($contentRepository); + + $this->nodeAddress = $nodeAddressFactory->createFromNode($this->node); + $this->parentNodeAddress = $nodeAddressFactory->createFromNode($this->parentNode); + } + public function getNode(): Node { return $this->node; @@ -87,11 +101,9 @@ public function isSimilarTo(FeedbackInterface $feedback) */ public function serializePayload(ControllerContext $controllerContext) { - $contentRepository = $this->contentRepositoryRegistry->get($this->node->subgraphIdentity->contentRepositoryId); - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); return [ - 'contextPath' => $nodeAddressFactory->createFromNode($this->node)->serializeForUri(), - 'parentContextPath' => $nodeAddressFactory->createFromNode($this->parentNode)->serializeForUri() + 'contextPath' => $this->nodeAddress->serializeForUri(), + 'parentContextPath' => $this->parentNodeAddress->serializeForUri() ]; } } diff --git a/Tests/IntegrationTests/TestDistribution/composer.json b/Tests/IntegrationTests/TestDistribution/composer.json index c55b71c7b4..b23c99a6ce 100644 --- a/Tests/IntegrationTests/TestDistribution/composer.json +++ b/Tests/IntegrationTests/TestDistribution/composer.json @@ -6,7 +6,8 @@ "vendor-dir": "Packages/Libraries", "bin-dir": "bin", "allow-plugins": { - "neos/composer-plugin": true + "neos/composer-plugin": true, + "cweagans/composer-patches": true } }, "require": { @@ -20,7 +21,16 @@ "neos/neos-ui-compiled": "9.0.x-dev as 9.0", "neos/test-site": "@dev", - "neos/test-nodetypes": "@dev" + "neos/test-nodetypes": "@dev", + + "cweagans/composer-patches": "^1.7.3" + }, + "extra": { + "patches": { + "neos/neos-development-collection": { + "!!!FEATURE: (Neos.Neos) Track created nodes in PendingChangesProjection": "https://github.com/neos/neos-development-collection/pull/4393.patch" + } + } }, "repositories": { "distribution": { diff --git a/packages/neos-ui-sagas/src/UI/ContentTree/index.js b/packages/neos-ui-sagas/src/UI/ContentTree/index.js index a68d024ac1..adcdd4df6c 100644 --- a/packages/neos-ui-sagas/src/UI/ContentTree/index.js +++ b/packages/neos-ui-sagas/src/UI/ContentTree/index.js @@ -142,7 +142,7 @@ export function * watchCurrentDocument({globalRegistry, configuration}) { } const state = yield select(); - const childrenAreFullyLoaded = $get(['cr', 'nodes', 'byContextPath', contextPath, 'children'], state) + const childrenAreFullyLoaded = ($get(['cr', 'nodes', 'byContextPath', contextPath, 'children'], state) || []) .filter(childEnvelope => nodeTypesRegistry.hasRole(childEnvelope.nodeType, 'content') || nodeTypesRegistry.hasRole(childEnvelope.nodeType, 'contentCollection')) .every( childEnvelope => Boolean($get(['cr', 'nodes', 'byContextPath', $get('contextPath', childEnvelope)], state))