From a84645d81a390f6ea7d33fac4642a699612d2485 Mon Sep 17 00:00:00 2001 From: Markus Timtner Date: Tue, 3 Nov 2026 15:23:06 +0100 Subject: [PATCH] [BUGFIX] move all children of a container in configured column order The children of a moved or copied container are handled column by column, and the target of a column is the last child of the previous column (see ContainerService::getNewContentElementAtTopTargetInColumn()). The column order was taken from the children records, which are fetched ORDER BY sorting: if the child with the lowest sorting is not in the first column, the target of another column is a child that has not been moved yet. That target resolves to the pid of the source page, so the child stays behind on it. Use the configured column order of the container CType instead, the same order the target is resolved with. Children with a colPos that is not configured for the container are still moved, after the others. Fixes: #291 Related: #738 --- .../CommandMapPostProcessingHook.php | 18 +++++++++-- .../DefaultLanguage/ContainerTest.php | 31 +++++++++++++++++++ ...ChildSortingBeforePreviousColumnResult.csv | 11 +++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerOtherPageOnTopWithChildSortingBeforePreviousColumnResult.csv diff --git a/Classes/Hooks/Datahandler/CommandMapPostProcessingHook.php b/Classes/Hooks/Datahandler/CommandMapPostProcessingHook.php index 28b7b57b..a63bcd15 100644 --- a/Classes/Hooks/Datahandler/CommandMapPostProcessingHook.php +++ b/Classes/Hooks/Datahandler/CommandMapPostProcessingHook.php @@ -15,6 +15,7 @@ use B13\Container\Domain\Factory\ContainerFactory; use B13\Container\Domain\Factory\Exception; 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; @@ -24,8 +25,11 @@ #[Autoconfigure(public: true)] class CommandMapPostProcessingHook { - public function __construct(protected ContainerFactory $containerFactory, protected ContainerService $containerService) - { + public function __construct( + protected ContainerFactory $containerFactory, + protected ContainerService $containerService, + protected Registry $tcaRegistry + ) { } public function processCmdmap_postProcess(string $command, string $table, $id, $value, DataHandler $dataHandler, $pasteUpdate, $pasteDatamap): void @@ -145,7 +149,17 @@ protected function copyOrMoveChildren(int $origUid, int $newId, int $containerId && isset($allChildren[0]) && (int)$targetLanguage !== (int)$allChildren[0]['sys_language_uid']; $children = []; + // the target of a column is the last child of the previous column (see + // ContainerService::getNewContentElementAtTopTargetInColumn()), so the columns have to be + // processed in the configured order: the children are ordered by sorting, and a column + // whose child has a low sorting must not be processed before the column it targets. + // children with a colPos that is not configured for the container are moved last $colPosVals = $container->getChildrenColPos(); + $availableColPosVals = $this->tcaRegistry->getAllAvailableColumnsColPos($container->getCType()); + $colPosVals = array_values(array_merge( + array_intersect($availableColPosVals, $colPosVals), + array_diff($colPosVals, $availableColPosVals) + )); foreach ($colPosVals as $colPos) { $childrenByColPos = $container->getChildrenByColPos($colPos); if (!$chainAfterPrevious) { diff --git a/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php b/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php index 8e79b212..eccd2277 100644 --- a/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php +++ b/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php @@ -414,4 +414,35 @@ public function copyContainerWithLanguageAsStringKeepsCopiedChildrenSorting(): v $this->dataHandler->process_cmdmap(); self::assertCSVDataSet(__DIR__ . '/Fixtures/Container/CopyContainerKeepsSortingOfChildrenResult.csv'); } + + /** + * children are moved column by column, the target of a column is the last child of the previous + * column. the children are fetched ordered by sorting, so if the child with the lowest sorting is + * not in the first column, the target of the following columns is a child that has not been moved + * yet and the child ends up on the source page. + * + * such a sorting is inconsistent and is reported by the container:sorting command, but it can be + * the result of copy and move operations or of an import, so a container has to be moved completely + * in that case, too. + * + * @see https://github.com/b13/container/issues/291 + * @see https://github.com/b13/container/issues/738 + */ + #[Test] + public function moveContainerOtherPageOnTopWithChildSortingBeforePreviousColumn(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerOtherPageOnTop.csv'); + // child in the second column gets a sorting lower than the children of the first column + $this->getConnectionPool()->getConnectionForTable('tt_content')->update('tt_content', ['sorting' => 96], ['uid' => 4]); + $cmdmap = [ + 'tt_content' => [ + 1 => [ + 'move' => 3, + ], + ], + ]; + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerOtherPageOnTopWithChildSortingBeforePreviousColumnResult.csv'); + } } diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerOtherPageOnTopWithChildSortingBeforePreviousColumnResult.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerOtherPageOnTopWithChildSortingBeforePreviousColumnResult.csv new file mode 100644 index 00000000..75322627 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerOtherPageOnTopWithChildSortingBeforePreviousColumnResult.csv @@ -0,0 +1,11 @@ +"pages" +,"uid","pid","title" +,1,0,"" +,3,0,"" +"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","col1-first",320,0,200,1,0 +,3,3,"header","col1-second",384,0,200,1,0 +,4,3,"header","col2-first",448,0,201,1,0 +,10,3,"header","outside",512,0,0,0,0