diff --git a/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php b/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php index 91287333..7a000a6c 100644 --- a/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php +++ b/Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php @@ -134,6 +134,22 @@ protected function rewriteCommandMapTargetForAfterContainer(array $cmdmap): arra if (!$this->tcaRegistry->isContainerElement($record['CType'])) { continue; } + // Page module "drop after this container" sends target=-uid of the moved + // container itself. Rewriting that to "after last child" keeps the record + // in the same column slot (children sort between parent and next sibling), + // so move-down is a no-op. Retarget to the next sibling instead. + if ($operation === 'move' && (int)$id === $target) { + $nextTarget = $this->getMoveAfterSelfTarget($record); + if ($nextTarget === null) { + continue; + } + if (is_array($value)) { + $cmd[$operation]['target'] = $nextTarget; + } else { + $cmd[$operation] = $nextTarget; + } + continue; + } try { $container = $this->containerFactory->buildContainer((int)$record['uid']); $target = $this->containerService->getAfterContainerElementTarget($container); @@ -153,6 +169,27 @@ protected function rewriteCommandMapTargetForAfterContainer(array $cmdmap): arra return $cmdmap; } + /** + * "Move after this container" for the container itself means one slot down + * in the same page column, not after its last child. + */ + protected function getMoveAfterSelfTarget(array $record): ?int + { + $next = $this->database->fetchNextSiblingRecord($record, (int)$record['sorting']); + if ($next === null) { + return null; + } + if (!$this->tcaRegistry->isContainerElement((string)$next['CType'])) { + return -((int)$next['uid']); + } + try { + $nextContainer = $this->containerFactory->buildContainer((int)$next['uid']); + return $this->containerService->getAfterContainerElementTarget($nextContainer); + } catch (Exception $e) { + return -((int)$next['uid']); + } + } + protected function rewriteCommandMapTargetForTopAtContainer(array $cmdmap): array { if (!empty($cmdmap['tt_content'])) { diff --git a/Classes/Hooks/Datahandler/Database.php b/Classes/Hooks/Datahandler/Database.php index 0b06e6a8..ce31f1dd 100644 --- a/Classes/Hooks/Datahandler/Database.php +++ b/Classes/Hooks/Datahandler/Database.php @@ -113,6 +113,51 @@ public function fetchOneTranslatedRecordByLocalizationParent(int $uid, int $lang return $record; } + public function fetchNextSiblingRecord(array $record, int $afterSorting): ?array + { + $queryBuilder = $this->getQueryBuilder(); + $row = $queryBuilder->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter((int)$record['pid'], Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'colPos', + $queryBuilder->createNamedParameter((int)$record['colPos'], Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'sys_language_uid', + $queryBuilder->createNamedParameter((int)$record['sys_language_uid'], Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'tx_container_parent', + $queryBuilder->createNamedParameter((int)$record['tx_container_parent'], Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 't3ver_oid', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'sorting', + $queryBuilder->createNamedParameter($afterSorting, Connection::PARAM_INT) + ), + $queryBuilder->expr()->neq( + 'uid', + $queryBuilder->createNamedParameter((int)$record['uid'], Connection::PARAM_INT) + ) + ) + ->orderBy('sorting', 'ASC') + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + if ($row === false) { + return null; + } + return $row; + } + public function fetchRecordsByParentAndLanguage(int $parent, int $language): array { $queryBuilder = $this->getQueryBuilder(); diff --git a/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php b/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php index 29551388..b351a91a 100644 --- a/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php +++ b/Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php @@ -130,6 +130,74 @@ public function moveContainerAfterElementMovesChildren(): void self::assertCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterElementMovesChildrenResult.csv'); } + #[Test] + public function moveContainerAfterItselfMovesAfterNextElement(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterElementMovesChildren.csv'); + $cmdmap = [ + 'tt_content' => [ + 1 => [ + 'move' => [ + 'action' => 'paste', + 'target' => -1, + 'update' => [ + 'colPos' => 0, + 'sys_language_uid' => 0, + ], + ], + ], + ], + ]; + + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_datamap(); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterElementMovesChildrenResult.csv'); + } + + #[Test] + public function moveContainerAfterItselfWithSimpleCommandMapMovesAfterNextElement(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterElementMovesChildren.csv'); + $cmdmap = [ + 'tt_content' => [ + 1 => [ + 'move' => -1, + ], + ], + ]; + + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_datamap(); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterElementMovesChildrenResult.csv'); + } + + #[Test] + public function moveContainerAfterItselfWhenNextIsContainerMovesAfterThatContainer(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterItselfNextIsContainer.csv'); + $cmdmap = [ + 'tt_content' => [ + 1 => [ + 'move' => [ + 'action' => 'paste', + 'target' => -1, + 'update' => [ + 'colPos' => 0, + 'sys_language_uid' => 0, + ], + ], + ], + ], + ]; + + $this->dataHandler->start([], $cmdmap, $this->backendUser); + $this->dataHandler->process_datamap(); + $this->dataHandler->process_cmdmap(); + self::assertCSVDataSet(__DIR__ . '/Fixtures/Container/MoveContainerAfterItselfNextIsContainerResult.csv'); + } + #[Test] public function moveContainerToOtherPageAtTopMovesChildren(): void { diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerAfterItselfNextIsContainer.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerAfterItselfNextIsContainer.csv new file mode 100644 index 00000000..6f9b0438 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerAfterItselfNextIsContainer.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,1,1,"b13-2cols-with-header-container","container-a",64,0,0,0,0 +,2,1,"header","child-a",128,0,200,1,0 +,10,1,"b13-2cols-with-header-container","container-b",256,0,0,0,0 +,11,1,"header","child-b",320,0,200,10,0 diff --git a/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerAfterItselfNextIsContainerResult.csv b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerAfterItselfNextIsContainerResult.csv new file mode 100644 index 00000000..65692ad5 --- /dev/null +++ b/Tests/Functional/Datahandler/DefaultLanguage/Fixtures/Container/MoveContainerAfterItselfNextIsContainerResult.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title" +,1,0,"page-1" +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,10,1,"b13-2cols-with-header-container","container-b",256,0,0,0,0 +,11,1,"header","child-b",320,0,200,10,0 +,1,1,"b13-2cols-with-header-container","container-a",576,0,0,0,0 +,2,1,"header","child-a",832,0,200,1,0 diff --git a/Tests/Unit/Hooks/Datahandler/CommandMapBeforeStartHookTest.php b/Tests/Unit/Hooks/Datahandler/CommandMapBeforeStartHookTest.php index ea259bfc..f971f2df 100644 --- a/Tests/Unit/Hooks/Datahandler/CommandMapBeforeStartHookTest.php +++ b/Tests/Unit/Hooks/Datahandler/CommandMapBeforeStartHookTest.php @@ -173,6 +173,73 @@ public function rewriteSimpleCommandMapTestForAfterContainer(): void self::assertSame($expected, $rewrittenCommandMap); } + #[Test] + public function rewriteCommandMapTargetForAfterContainerMovesContainerAfterNextSibling(): void + { + $movedContainer = [ + 'uid' => 1, + 'pid' => 1, + 'sorting' => 64, + 'tx_container_parent' => 0, + 'sys_language_uid' => 0, + 'colPos' => 0, + 'CType' => 'container-ctype', + ]; + $nextSibling = [ + 'uid' => 4, + 'pid' => 1, + 'sorting' => 512, + 'tx_container_parent' => 0, + 'sys_language_uid' => 0, + 'colPos' => 0, + 'CType' => 'header', + ]; + + $containerFactory = $this->getMockBuilder(ContainerFactory::class)->disableOriginalConstructor()->getMock(); + $containerService = $this->getMockBuilder(ContainerService::class)->disableOriginalConstructor()->getMock(); + $database = $this->getMockBuilder(Database::class)->onlyMethods(['fetchOneRecord', 'fetchNextSiblingRecord'])->getMock(); + $tcaRegistry = $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->onlyMethods(['isContainerElement'])->getMock(); + $database->expects(self::once())->method('fetchOneRecord')->with(1)->willReturn($movedContainer); + $database->expects(self::once())->method('fetchNextSiblingRecord')->with($movedContainer, 64)->willReturn($nextSibling); + $tcaRegistry->method('isContainerElement')->willReturnCallback(static function (string $cType): bool { + return $cType === 'container-ctype'; + }); + $dataHandlerHook = $this->getMockBuilder($this->buildAccessibleProxy(CommandMapBeforeStartHook::class)) + ->setConstructorArgs(['containerFactory' => $containerFactory, 'tcaRegistry' => $tcaRegistry, 'database' => $database, 'containerService' => $containerService]) + ->onlyMethods([]) + ->getMock(); + $commandMap = [ + 'tt_content' => [ + 1 => [ + 'move' => [ + 'action' => 'paste', + 'target' => -1, + 'update' => [ + 'colPos' => 0, + 'sys_language_uid' => 0, + ], + ], + ], + ], + ]; + $expected = [ + 'tt_content' => [ + 1 => [ + 'move' => [ + 'action' => 'paste', + 'target' => -4, + 'update' => [ + 'colPos' => 0, + 'sys_language_uid' => 0, + ], + ], + ], + ], + ]; + $rewrittenCommandMap = $dataHandlerHook->_call('rewriteCommandMapTargetForAfterContainer', $commandMap); + self::assertSame($expected, $rewrittenCommandMap); + } + #[Test] public function setContainerIdToZeroIfNotSetOnUpdateSetsContainerIdToZeroValue(): void {