diff --git a/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php b/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php index 91287333..e6f2a52e 100644 --- a/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php +++ b/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php @@ -17,6 +17,7 @@ use B13\Container\Domain\Service\ContainerService; use B13\Container\Tca\Registry; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; +use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\DataHandling\DataHandler; #[Autoconfigure(public: true)] @@ -36,6 +37,9 @@ public function processCmdmap_beforeStart(DataHandler $dataHandler): void $dataHandler->cmdmap = $this->rewriteSimpleCommandMap($dataHandler->cmdmap); $dataHandler->cmdmap = $this->setContainerIdToZeroIfNotSetOnUpdate($dataHandler->cmdmap); $this->unsetInconsistentCopyOrMoveCommands($dataHandler); + // children are copied/moved together with their container (CommandMapPostProcessingHook), + // an additional command for a child would duplicate it (e.g. "select all" in Record module) + $this->unsetCopyOrMoveCommandsForChildrenOfContainersInCmdmap($dataHandler); // previously page id is used for copy/moving element at top of a container colum // but this leeds to wrong sorting in page context (e.g. List-Module) $dataHandler->cmdmap = $this->rewriteCommandMapTargetForTopAtContainer($dataHandler->cmdmap); @@ -89,6 +93,62 @@ protected function unsetInconsistentCopyOrMoveCommands(DataHandler $dataHandler) } } + protected function unsetCopyOrMoveCommandsForChildrenOfContainersInCmdmap(DataHandler $dataHandler): void + { + $commands = $dataHandler->cmdmap['tt_content'] ?? []; + if (count($commands) < 2) { + return; + } + $workspaceId = (int)($dataHandler->BE_USER->workspace ?? 0); + foreach ($commands as $id => $cmds) { + if (!is_array($cmds)) { + continue; + } + foreach (['copy', 'move'] as $operation) { + if (!isset($cmds[$operation])) { + continue; + } + if ($this->hasAncestorContainerWithSameCommand((int)$id, $operation, $cmds[$operation], $commands, $workspaceId)) { + unset($dataHandler->cmdmap['tt_content'][$id][$operation]); + } + } + if (empty($dataHandler->cmdmap['tt_content'][$id])) { + unset($dataHandler->cmdmap['tt_content'][$id]); + } + } + } + + /** + * a child command is only redundant if it is identical to the command of one of its containers, + * a child with an own target (e.g. pasted into another container) is kept + */ + protected function hasAncestorContainerWithSameCommand(int $id, string $operation, mixed $command, array $commands, int $workspaceId): bool + { + $visited = [$id => true]; + $parentId = $this->fetchContainerParent($id, $workspaceId); + while ($parentId > 0 && !isset($visited[$parentId])) { + if (isset($commands[$parentId][$operation]) && $commands[$parentId][$operation] == $command) { + return true; + } + $visited[$parentId] = true; + $parentId = $this->fetchContainerParent($parentId, $workspaceId); + } + return false; + } + + protected function fetchContainerParent(int $uid, int $workspaceId): int + { + $record = $this->database->fetchOneRecord($uid); + if ($record !== null && $workspaceId > 0) { + // the child may have been moved in/out of a container in the workspace + BackendUtility::workspaceOL('tt_content', $record, $workspaceId, true); + } + if (!is_array($record)) { + return 0; + } + return (int)($record['tx_container_parent'] ?? 0); + } + protected function setContainerIdToZeroIfNotSetOnUpdate(array $cmdmap): array { if (!empty($cmdmap['tt_content'])) { diff --git a/Tests/Functional/Datahandler/DefaultLanguage/ContainerWithChildrenInCmdmapTest.php b/Tests/Functional/Datahandler/DefaultLanguage/ContainerWithChildrenInCmdmapTest.php new file mode 100644 index 00000000..7f300588 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/ContainerWithChildrenInCmdmapTest.php @@ -0,0 +1,71 @@ +importCSVDataSet(__DIR__ . '/Fixtures/ContainerWithChildrenInCmdmap/Container.csv'); + $cmdmap = [ + 'tt_content' => [ + 3 => ['copy' => 3], + 2 => ['copy' => 3], + 1 => ['copy' => 3], + ], + ]; + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/ContainerWithChildrenInCmdmap/CopyContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv'); + } + + #[Test] + public function moveContainerWithChildrenInCmdmapKeepsChildrenInContainer(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/ContainerWithChildrenInCmdmap/Container.csv'); + $cmdmap = [ + 'tt_content' => [ + 3 => ['move' => 3], + 2 => ['move' => 3], + 1 => ['move' => 3], + ], + ]; + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/ContainerWithChildrenInCmdmap/MoveContainerWithChildrenInCmdmapKeepsChildrenInContainerResult.csv'); + } + + #[Test] + public function copyNestedContainerWithChildrenInCmdmapCopiesChildrenOnlyOnce(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/ContainerWithChildrenInCmdmap/NestedContainer.csv'); + $cmdmap = [ + 'tt_content' => [ + 3 => ['copy' => 3], + 2 => ['copy' => 3], + 1 => ['copy' => 3], + ], + ]; + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/ContainerWithChildrenInCmdmap/CopyNestedContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv'); + } +} diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/Container.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/Container.csv new file mode 100644 index 00000000..a8ad5c29 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/Container.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +,3,0,"page-2" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,1,1,"b13-2cols-with-header-container","container",64,0,0,0,0 +,2,1,"header","col-200",128,0,200,1,0 +,3,1,"header","col-201",192,0,201,1,0 diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/CopyContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/CopyContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv new file mode 100644 index 00000000..8a812ce3 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/CopyContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv @@ -0,0 +1,12 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +,3,0,"page-2" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,1,1,"b13-2cols-with-header-container","container",64,0,0,0,0 +,2,1,"header","col-200",128,0,200,1,0 +,3,1,"header","col-201",192,0,201,1,0 +,4,3,"b13-2cols-with-header-container","container",256,0,0,0,0 +,5,3,"header","col-200",512,0,200,4,0 +,6,3,"header","col-201",768,0,201,4,0 diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/CopyNestedContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/CopyNestedContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv new file mode 100644 index 00000000..50f7e949 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/CopyNestedContainerWithChildrenInCmdmapCopiesChildrenOnlyOnceResult.csv @@ -0,0 +1,12 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +,3,0,"page-2" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,1,1,"b13-2cols-with-header-container","container",64,0,0,0,0 +,2,1,"b13-2cols-with-header-container","child-container",128,0,200,1,0 +,3,1,"header","content-in-child-container",192,0,200,2,0 +,4,3,"b13-2cols-with-header-container","container",256,0,0,0,0 +,5,3,"b13-2cols-with-header-container","child-container",512,0,200,4,0 +,6,3,"header","content-in-child-container",768,0,200,5,0 diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/MoveContainerWithChildrenInCmdmapKeepsChildrenInContainerResult.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/MoveContainerWithChildrenInCmdmapKeepsChildrenInContainerResult.csv new file mode 100644 index 00000000..9681cd72 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/MoveContainerWithChildrenInCmdmapKeepsChildrenInContainerResult.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +,3,0,"page-2" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,1,3,"b13-2cols-with-header-container","container",256,0,0,0,0 +,2,3,"header","col-200",512,0,200,1,0 +,3,3,"header","col-201",768,0,201,1,0 diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/NestedContainer.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/NestedContainer.csv new file mode 100644 index 00000000..f20f888b --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/ContainerWithChildrenInCmdmap/NestedContainer.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +,3,0,"page-2" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,1,1,"b13-2cols-with-header-container","container",64,0,0,0,0 +,2,1,"b13-2cols-with-header-container","child-container",128,0,200,1,0 +,3,1,"header","content-in-child-container",192,0,200,2,0 diff --git a/Tests/Functional/Datahandler/Workspace/ContainerTest.php b/Tests/Functional/Datahandler/Workspace/ContainerTest.php index 70f11db9..6ed4426a 100644 --- a/Tests/Functional/Datahandler/Workspace/ContainerTest.php +++ b/Tests/Functional/Datahandler/Workspace/ContainerTest.php @@ -314,4 +314,20 @@ public function deleteContainerWithChildHasDeletedPlaceholderInWorkspaceDoNotDis $this->dataHandler->process_cmdmap(); self::assertCSVDataSet(__DIR__ . '/Fixtures/DeleteContainerWithChildHasDeletedPlaceholderInWorkspaceDoNotDiscardThisChildResult.csv'); } + + #[Test] + public function copyContainerWithChildrenInCmdmapCopiesChildMovedOutOfContainerInWorkspace(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainer.csv'); + $cmdmap = [ + 'tt_content' => [ + 3 => ['copy' => 3], + 2 => ['copy' => 3], + 1 => ['copy' => 3], + ], + ]; + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainerResult.csv'); + } } diff --git a/Tests/Functional/Datahandler/Workspace/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainer.csv b/Tests/Functional/Datahandler/Workspace/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainer.csv new file mode 100644 index 00000000..2650fab4 --- /dev/null +++ b/Tests/Functional/Datahandler/Workspace/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainer.csv @@ -0,0 +1,6 @@ +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent","l10n_source","t3ver_oid","t3ver_wsid","t3ver_state","t3ver_stage" +,1,1,"b13-2cols-with-header-container","container",128,0,0,0,0,0,0,0,0,0 +,2,1,"header","moved-out-in-workspace",256,0,200,1,0,0,0,0,0,0 +,3,1,"header","child",512,0,201,1,0,0,0,0,0,0 +,4,1,"header","moved-out-in-workspace",1024,0,0,0,0,0,2,1,4,0 diff --git a/Tests/Functional/Datahandler/Workspace/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainerResult.csv b/Tests/Functional/Datahandler/Workspace/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainerResult.csv new file mode 100644 index 00000000..3b9d841b --- /dev/null +++ b/Tests/Functional/Datahandler/Workspace/Fixtures/CopyContainerWithChildrenInCmdmapChildMovedOutOfContainerResult.csv @@ -0,0 +1,9 @@ +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent","l10n_source","t3ver_oid","t3ver_wsid","t3ver_state","t3ver_stage" +,1,1,"b13-2cols-with-header-container","container",128,0,0,0,0,0,0,0,0,0 +,2,1,"header","moved-out-in-workspace",256,0,200,1,0,0,0,0,0,0 +,3,1,"header","child",512,0,201,1,0,0,0,0,0,0 +,4,1,"header","moved-out-in-workspace",1024,0,0,0,0,0,2,1,4,0 +,5,3,"header","moved-out-in-workspace",256,0,0,0,0,0,0,1,1,0 +,6,3,"b13-2cols-with-header-container","container",128,0,0,0,0,0,0,1,1,0 +,7,3,"header","child",192,0,201,6,0,0,0,1,1,0