From 892bf21b3800e433ef56100e9527ef097e349247 Mon Sep 17 00:00:00 2001 From: Achim Fritz Date: Sat, 24 Jan 2026 11:12:52 +0100 Subject: [PATCH] [BUGFIX] record summay for localization before v14 it was possible to list records in any colPos in LocalizationWizard. We have the RecordSummaryForLocalization Listener to modify colums and records to be displayd in the LocalizationWizard. With v14 backend layout is used to render translateable records and so only columns with known colPos in backend layout are rendered. s. https://review.typo3.org/c/Packages/TYPO3.CMS/+/90489 this PR changed the colPos of container children and move them into the outer container colPos which exists in the backend layout. e.g. a children in a container has colPos 200, and container has colPos 0 in the backend layout and the container is translated but the children not we changed the children colPos to the container colPos so it is displayed in the Localazation Wizard in colPos 0. if the container is also in translateable records, the childrens are not listed, as before. They will automatically localized in DataHandler Hook. --- .../Listener/RecordSummaryForLocalization.php | 112 +++++++++++++++++- .../RecordSummaryForLocalizationException.php | 19 +++ Tests/Acceptance/Backend/LayoutCest.php | 10 +- .../Listener/Fixtures/localize_container.csv | 8 ++ .../Fixtures/localize_container_child.csv | 11 ++ .../Listener/RecordSummaryForLocalization.php | 94 +++++++++++++++ .../RecordLocalizeSummaryModifierTest.php | 10 ++ 7 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 Classes/Listener/RecordSummaryForLocalizationException.php create mode 100644 Tests/Functional/Listener/Fixtures/localize_container.csv create mode 100644 Tests/Functional/Listener/Fixtures/localize_container_child.csv create mode 100644 Tests/Functional/Listener/RecordSummaryForLocalization.php diff --git a/Classes/Listener/RecordSummaryForLocalization.php b/Classes/Listener/RecordSummaryForLocalization.php index c3d71a6e..d75236bf 100644 --- a/Classes/Listener/RecordSummaryForLocalization.php +++ b/Classes/Listener/RecordSummaryForLocalization.php @@ -14,6 +14,9 @@ use B13\Container\Service\RecordLocalizeSummaryModifier; use TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Information\Typo3Version; class RecordSummaryForLocalization { @@ -22,18 +25,115 @@ class RecordSummaryForLocalization */ protected $recordLocalizeSummaryModifier; - public function __construct(RecordLocalizeSummaryModifier $recordLocalizeSummaryModifier) - { + protected ConnectionPool $connectionPool; + + public function __construct( + RecordLocalizeSummaryModifier $recordLocalizeSummaryModifier, + ConnectionPool $connectionPool + ) { $this->recordLocalizeSummaryModifier = $recordLocalizeSummaryModifier; + $this->connectionPool = $connectionPool; } public function __invoke(AfterRecordSummaryForLocalizationEvent $event): void { $records = $event->getRecords(); - $columns = $event->getColumns(); - $records = $this->recordLocalizeSummaryModifier->filterRecords($records); - $columns = $this->recordLocalizeSummaryModifier->rebuildColumns($columns); - $event->setColumns($columns); + if ((new Typo3Version())->getMajorVersion() < 14) { + $columns = $event->getColumns(); + $records = $this->recordLocalizeSummaryModifier->filterRecords($records); + $columns = $this->recordLocalizeSummaryModifier->rebuildColumns($columns); + $event->setColumns($columns); + $event->setRecords($records); + return; + } + $localizeRecords = []; + foreach ($records as $colPos => $recordsPerColPos) { + foreach ($recordsPerColPos as $record) { + $localizeRecords[$record['uid']] = $record; + } + } + $fullRecords = $this->fetchAllRecords(array_keys($localizeRecords)); + $records = $this->moveContainerColPosIntoPageColPos($fullRecords, $localizeRecords); $event->setRecords($records); } + + protected function moveContainerColPosIntoPageColPos(array $records, array $localizeRecords): array + { + $recordsPerColPos = []; + foreach ($records as $record) { + $colPos = $this->resolveRecordColPos($record, $localizeRecords); + if (!isset($recordsPerColPos[$colPos])) { + $recordsPerColPos[$colPos] = []; + } + if (!isset($localizeRecords[$record['uid']])) { + throw new RecordSummaryForLocalizationException('localizeRecord not set ' . $record['uid'], 1769247959); + } + $recordsPerColPos[$colPos][] = $localizeRecords[$record['uid']]; + } + return $recordsPerColPos; + } + + protected function resolveRecordColPos(array $record, $localizeRecords): int + { + if (($record['tx_container_parent'] ?? 0) === 0) { + return $record['colPos']; + } + $loopCnt = 0; + $maxDepth = 20; + $containerUid = $record['tx_container_parent']; + while (true) { + if (in_array($containerUid, array_keys($localizeRecords))) { + return $record['colPos']; + } + if ($loopCnt > $maxDepth) { + throw new RecordSummaryForLocalizationException('maxDepth has reached ' . $maxDepth, 1769247958); + } + $containerRecord = $this->fetchOneRecord($containerUid); + if ($containerRecord === null) { + throw new RecordSummaryForLocalizationException('cannot fetch record for uid ' . $containerUid, 1769247957); + } + if (($containerRecord['tx_container_parent'] ?? 0) === 0) { + return $containerRecord['colPos']; + } + $containerUid = $containerRecord['tx_container_parent']; + $loopCnt++; + } + } + + protected function fetchOneRecord(int $uid): ?array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $row = $queryBuilder->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'uid', + $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchAssociative(); + return $row ?: null; + } + + protected function fetchAllRecords(array $uids): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $rows = $queryBuilder->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY) + ) + ) + ->orderBy('sorting') + ->executeQuery() + ->fetchAllAssociative(); + $rowsPerUid = []; + foreach ($rows as $row) { + $rowsPerUid[$row['uid']] = $row; + } + return $rowsPerUid; + } } diff --git a/Classes/Listener/RecordSummaryForLocalizationException.php b/Classes/Listener/RecordSummaryForLocalizationException.php new file mode 100644 index 00000000..f0bb1f94 --- /dev/null +++ b/Classes/Listener/RecordSummaryForLocalizationException.php @@ -0,0 +1,19 @@ +click('a.t3js-localize'); } else { $I->waitForText('Translate'); - $I->executeJS("document.querySelector('typo3-backend-localization-button').click()"); - // AfterRecordSummaryForLocalizationEvent - $scenario->skip('need more work, AfterRecordSummaryForLocalizationEvent needs refactoring'); + $I->executeJS("document.querySelector('#PageLayoutController typo3-backend-localization-button').click()"); } $I->switchToIFrame(); @@ -405,7 +403,11 @@ public function canTranslateChildWithTranslationModule(BackendTester $I, PageTre $I->waitForElement('.t3js-localization-option'); $I->waitForElement('div[data-bs-slide="localize-summary"]'); } - $I->waitForText('(212) headerOfChild'); + if ($I->getTypo3MajorVersion() < 14) { + $I->waitForText('(212) headerOfChild'); + } else { + $I->waitForText('headerOfChild'); + } } /** diff --git a/Tests/Functional/Listener/Fixtures/localize_container.csv b/Tests/Functional/Listener/Fixtures/localize_container.csv new file mode 100644 index 00000000..7c2aad3b --- /dev/null +++ b/Tests/Functional/Listener/Fixtures/localize_container.csv @@ -0,0 +1,8 @@ +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,"4","1","header","ce 2","288","0","200","5","0" +,"5","1","b13-2cols-with-header-container","container","256","0","0","0","0" +"pages" +,"uid","pid","sys_language_uid","l10n_parent" +,"1","0","0","0" +,"2","0","1","1" diff --git a/Tests/Functional/Listener/Fixtures/localize_container_child.csv b/Tests/Functional/Listener/Fixtures/localize_container_child.csv new file mode 100644 index 00000000..eef94f2d --- /dev/null +++ b/Tests/Functional/Listener/Fixtures/localize_container_child.csv @@ -0,0 +1,11 @@ +"tt_content" +,"uid","pid","CType","header","sorting","sys_language_uid","colPos","tx_container_parent","l18n_parent" +,"4","1","header","ce 2","288","0","200","5","0" +,"5","1","b13-2cols-with-header-container","container","256","0","0","0","0" +,"38","1","b13-2cols-with-header-container","translated container","272","1","0","0","5" +,"40","1","header","last element","300","0","0","0","0" +,"41","1","header","first element","128","0","0","0","0" +"pages" +,"uid","pid","sys_language_uid","l10n_parent" +,"1","0","0","0" +,"2","0","1","1" diff --git a/Tests/Functional/Listener/RecordSummaryForLocalization.php b/Tests/Functional/Listener/RecordSummaryForLocalization.php new file mode 100644 index 00000000..e4c6730f --- /dev/null +++ b/Tests/Functional/Listener/RecordSummaryForLocalization.php @@ -0,0 +1,94 @@ +getMajorVersion() < 14) { + self::markTestSkipped('tested by RecordLocalizeSummaryModifierTest Unit Test'); + } + $records = [ + 0 => [ + 0 => ['uid' => 41, 'title' => 'first element'], + 1 => ['uid' => 40, 'title' => 'last element'], + ], + 200 => [ + 0 => ['uid' => 4, 'title' => 'ce 2'], + ], + ]; + $columns = [0 => 'Normal']; + $event = new AfterRecordSummaryForLocalizationEvent($records, $columns); + $this->importCSVDataSet(__DIR__ . '/Fixtures/localize_container_child.csv'); + $listener = $this->getContainer()->get(\B13\Container\Listener\RecordSummaryForLocalization::class); + $listener($event); + $records = $event->getRecords(); + $expected = [ + 0 => [ + 0 => ['uid' => 41, 'title' => 'first element'], + 1 => ['uid' => 4, 'title' => 'ce 2'], + 2 => ['uid' => 40, 'title' => 'last element'], + ], + ]; + self::assertSame($expected, $records); + } + + /** + * @test + */ + public function childrenIsNotMovedIntoBackendLayoutColPosIfContainerShouldBeTranslated(): void + { + if ((new Typo3Version())->getMajorVersion() < 14) { + self::markTestSkipped('tested by RecordLocalizeSummaryModifierTest Unit Test'); + } + $records = [ + 0 => [ + 0 => ['uid' => 5, 'title' => 'container'], + ], + 200 => [ + 0 => ['uid' => 4, 'title' => 'ce 2'], + ], + ]; + $columns = [0 => 'Normal']; + $event = new AfterRecordSummaryForLocalizationEvent($records, $columns); + $this->importCSVDataSet(__DIR__ . '/Fixtures/localize_container.csv'); + $listener = $this->getContainer()->get(\B13\Container\Listener\RecordSummaryForLocalization::class); + $listener($event); + $records = $event->getRecords(); + $expected = [ + 0 => [ + 0 => ['uid' => 5, 'title' => 'container'], + ], + 200 => [ + 0 => ['uid' => 4, 'title' => 'ce 2'], + ], + ]; + self::assertSame($expected, $records); + } +} diff --git a/Tests/Unit/Service/RecordLocalizeSummaryModifierTest.php b/Tests/Unit/Service/RecordLocalizeSummaryModifierTest.php index d1224a22..3e3a6290 100644 --- a/Tests/Unit/Service/RecordLocalizeSummaryModifierTest.php +++ b/Tests/Unit/Service/RecordLocalizeSummaryModifierTest.php @@ -14,6 +14,7 @@ use B13\Container\Service\RecordLocalizeSummaryModifier; use B13\Container\Tca\Registry; +use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; class RecordLocalizeSummaryModifierTest extends UnitTestCase @@ -25,6 +26,9 @@ class RecordLocalizeSummaryModifierTest extends UnitTestCase */ public function filterRecordsRemovesContainerChildrenIfParentContainerIsTranslatedAsWell(): void { + if ((new Typo3Version())->getMajorVersion() >= 14) { + self::markTestSkipped('not used in v14'); + } $recordLocalizeSummeryModifier = $this->getAccessibleMock( RecordLocalizeSummaryModifier::class, ['getContainerUids', 'getContainerChildren'], @@ -48,6 +52,9 @@ public function filterRecordsRemovesContainerChildrenIfParentContainerIsTranslat */ public function filterRecordsKeepsContainerChildrenIfParentContainerIsNotTranslated(): void { + if ((new Typo3Version())->getMajorVersion() >= 14) { + self::markTestSkipped('not used in v14'); + } $recordLocalizeSummeryModifier = $this->getAccessibleMock( RecordLocalizeSummaryModifier::class, ['getContainerUids', 'getContainerChildren'], @@ -71,6 +78,9 @@ public function filterRecordsKeepsContainerChildrenIfParentContainerIsNotTransla */ public function rebuildColumnsReturnsColumnListWithConsecutiveArrayKeysAlsoWhenRegistryReturnsRepeatingColumns(): void { + if ((new Typo3Version())->getMajorVersion() >= 14) { + self::markTestSkipped('not used in v14'); + } $tcaRegistry = $this->getMockBuilder(Registry::class) ->disableOriginalConstructor() ->onlyMethods(['getAllAvailableColumns'])