Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Classes/Hooks/Datahandler/CommandMapBeforeStartHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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'])) {
Expand Down
45 changes: 45 additions & 0 deletions Classes/Hooks/Datahandler/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
68 changes: 68 additions & 0 deletions Tests/Functional/Datahandler/DefaultLanguage/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions Tests/Unit/Hooks/Datahandler/CommandMapBeforeStartHookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down