diff --git a/packages/ep_theme/Classes/Command/MigrateGridelementsCommand.php b/packages/ep_theme/Classes/Command/MigrateGridelementsCommand.php new file mode 100644 index 0000000..ee7d9b4 --- /dev/null +++ b/packages/ep_theme/Classes/Command/MigrateGridelementsCommand.php @@ -0,0 +1,212 @@ + [container CType, first container colPos - 1, valid gridelements columns] + */ + private const LAYOUTS = [ + 'halves' => ['ep_container_halves', 200, [21, 22]], + 'thirds' => ['ep_container_thirds', 210, [31, 32, 33]], + 'quarters' => ['ep_container_columns_quarters', 220, [41, 42, 43, 44]], + 'halfAndTwoQuarters' => ['ep_container_half_and_two_quarters', 230, [51, 52, 53]], + 'twoQuartersAndHalf' => ['ep_container_two_quarters_and_half', 240, [61, 62, 63]], + ]; + + /** + * Content types whose template renders the header with settings.defaultHeaderType for layout 0 + */ + private const DEFAULT_HEADER_TYPES = ['text', 'textmedia', 'textpic']; + + private const RESET = [ + 'tx_gridelements_container' => 0, + 'tx_gridelements_columns' => 0, + 'tx_gridelements_children' => 0, + 'tx_gridelements_backend_layout' => '', + ]; + + public function __construct( + private readonly ConnectionPool $connectionPool, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Only show what would change') + ->addOption('report', null, InputOption::VALUE_REQUIRED, 'Write every row that is not migrated 1:1 to this CSV file'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $connection = $this->connectionPool->getConnectionForTable('tt_content'); + if (!$this->hasGridelementsColumns($connection)) { + $output->writeln('No tx_gridelements_* columns in tt_content, nothing to do.'); + return Command::SUCCESS; + } + + // Plain SQL on purpose: Connection::select() would apply the frontend restrictions (deleted, hidden, …) + $grids = []; + foreach ($connection->executeQuery('SELECT * FROM tt_content WHERE CType = ?', ['gridelements_pi1'])->iterateAssociative() as $row) { + $grids[(int)$row['uid']] = $row; + } + $children = $connection->executeQuery( + 'SELECT * FROM tt_content WHERE tx_gridelements_container > 0 AND CType <> ? ORDER BY uid', + ['gridelements_pi1'] + )->fetchAllAssociative(); + + if ((int)$connection->executeQuery('SELECT COUNT(*) FROM tt_content WHERE CType = ? AND sys_language_uid > 0', ['gridelements_pi1'])->fetchOne() > 0) { + $output->writeln('Translated grids found; translations are not handled by this command.'); + return Command::FAILURE; + } + + $updates = []; + $counts = []; + $report = []; + $count = static function (string $key) use (&$counts): void { + $counts[$key] = ($counts[$key] ?? 0) + 1; + }; + + foreach ($grids as $uid => $grid) { + $layout = (string)$grid['tx_gridelements_backend_layout']; + if (isset(self::LAYOUTS[$layout])) { + $updates[$uid] = ['CType' => self::LAYOUTS[$layout][0], 'pi_flexform' => ''] + self::RESET; + $count('grid ' . $layout . ((int)$grid['deleted'] ? ' (deleted)' : '')); + continue; + } + // A grid without layout renders nothing; keep it restorable as an empty container + $updates[$uid] = ['CType' => 'ep_container_halves', 'pi_flexform' => '', 'deleted' => 1] + self::RESET; + $count('grid without layout → deleted'); + if (!(int)$grid['deleted']) { + $report[] = [$grid, 'grid', 'grid without layout', 'deleted']; + } + } + + foreach ($children as $child) { + $uid = (int)$child['uid']; + $parent = $grids[(int)$child['tx_gridelements_container']] ?? null; + $column = (int)$child['tx_gridelements_columns']; + + if ((int)$child['colPos'] >= 0) { + // Rendered in its page column, not by the grid (gridelements only queries colPos -1) + $updates[$uid] = self::RESET; + $count('child outside the grid (colPos ' . $child['colPos'] . '), stays'); + if (!(int)$child['deleted']) { + $report[] = [$child, 'page column', 'colPos ' . $child['colPos'] . ' instead of -1', 'unchanged, grid fields cleared']; + } + continue; + } + if ($parent !== null && (int)$child['pid'] !== (int)$parent['pid']) { + // A container only renders children of its own page + $parent = null; + } + $config = self::LAYOUTS[(string)($parent['tx_gridelements_backend_layout'] ?? '')] ?? null; + if ($parent !== null && $config !== null && (int)$child['colPos'] === -1 && in_array($column, $config[2], true)) { + $updates[$uid] = [ + 'tx_container_parent' => (int)$parent['uid'], + 'colPos' => $config[1] + $column % 10, + ] + self::RESET; + $count('A: child migrated' . ((int)$child['deleted'] ? ' (deleted)' : '')); + // Inside a grid, TYPO3 10 did not apply settings.defaultHeaderType, so the header of these + // types stayed invisible with layout "default". Keep it invisible, and visible as such to editors. + if ((int)$child['header_layout'] === 0 && trim((string)$child['header']) !== '' && in_array($child['CType'], self::DEFAULT_HEADER_TYPES, true)) { + $updates[$uid]['header_layout'] = 100; + $count('A: header layout default → hidden'); + } + continue; + } + // B (column not in the layout) and C (parent is no grid): invisible in TYPO3 10, soft deleted + $group = $parent === null ? 'C' : 'B'; + $reason = $parent === null + ? 'parent ' . $child['tx_gridelements_container'] . ' is not a grid on the same page' + : 'column ' . $column . ' (colPos ' . $child['colPos'] . ') not in layout ' . $parent['tx_gridelements_backend_layout']; + $updates[$uid] = ['deleted' => 1] + self::RESET; + $count($group . ': child deleted' . ((int)$child['deleted'] ? ' (was deleted already)' : '')); + if (!(int)$child['deleted']) { + $report[] = [$child, $group, $reason, 'deleted']; + } + } + + // Other rows still carrying grid values (e.g. a former grid whose CType was changed by hand) + $leftovers = (int)$connection->executeQuery( + 'SELECT COUNT(*) FROM tt_content WHERE (tx_gridelements_children > 0 OR tx_gridelements_backend_layout NOT IN (\'\', \'0\')) AND CType <> ? AND tx_gridelements_container = 0', + ['gridelements_pi1'] + )->fetchOne(); + + ksort($counts); + foreach ($counts as $label => $number) { + $output->writeln(sprintf('%6d %s', $number, $label)); + } + if ($leftovers > 0) { + $output->writeln(sprintf('%6d other rows with grid values, cleared', $leftovers)); + } + if ($input->getOption('report')) { + $this->writeReport((string)$input->getOption('report'), $report); + $output->writeln(sprintf('Report: %d rows in %s', count($report), $input->getOption('report'))); + } + if ($updates === [] && $leftovers === 0) { + $output->writeln('Nothing to migrate.'); + return Command::SUCCESS; + } + if ($input->getOption('dry-run')) { + $output->writeln('Dry run, nothing written.'); + return Command::SUCCESS; + } + + $connection->transactional(function (Connection $connection) use ($updates): void { + foreach ($updates as $uid => $values) { + $connection->update('tt_content', $values, ['uid' => $uid]); + } + $connection->executeStatement( + 'UPDATE tt_content SET tx_gridelements_children = 0, tx_gridelements_backend_layout = \'\' WHERE tx_gridelements_children > 0 OR tx_gridelements_backend_layout NOT IN (\'\', \'0\')' + ); + }); + + $output->writeln(sprintf('%d rows migrated. Next: referenceindex:update, container:integrity, cache:flush', count($updates))); + return Command::SUCCESS; + } + + private function hasGridelementsColumns(Connection $connection): bool + { + return array_key_exists('tx_gridelements_container', $connection->createSchemaManager()->listTableColumns('tt_content')); + } + + /** + * @param list, 1: string, 2: string, 3: string}> $rows + */ + private function writeReport(string $file, array $rows): void + { + $titles = []; + foreach ($this->connectionPool->getConnectionForTable('pages')->select(['uid', 'title'], 'pages', ['sys_language_uid' => 0])->iterateAssociative() as $page) { + $titles[(int)$page['uid']] = $page['title']; + } + $handle = fopen($file, 'wb'); + fputcsv($handle, ['uid', 'pid', 'page', 'CType', 'header', 'hidden', 'group', 'reason', 'action'], escape: ''); + foreach ($rows as [$row, $group, $reason, $action]) { + fputcsv($handle, [ + $row['uid'], $row['pid'], $titles[(int)$row['pid']] ?? '', $row['CType'], $row['header'], $row['hidden'], $group, $reason, $action, + ], escape: ''); + } + fclose($handle); + } +} diff --git a/packages/ep_theme/Configuration/Icons.php b/packages/ep_theme/Configuration/Icons.php index c7e7025..5cf9b96 100644 --- a/packages/ep_theme/Configuration/Icons.php +++ b/packages/ep_theme/Configuration/Icons.php @@ -1,5 +1,6 @@ SvgIconProvider::class, 'source' => 'EXT:ep_theme/Resources/Public/images/logo.svg', ], + 'tx_eptheme_gridelements_halves' => [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:ep_theme/Resources/Public/images/tx_eptheme_gridelements_halves.png', + ], + 'tx_eptheme_gridelements_thirds' => [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:ep_theme/Resources/Public/images/tx_eptheme_gridelements_thirds.png', + ], + 'tx_eptheme_gridelements_quarters' => [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:ep_theme/Resources/Public/images/tx_eptheme_gridelements_quarters.png', + ], + 'tx_eptheme_gridelements_half_and_two_quarters' => [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:ep_theme/Resources/Public/images/tx_eptheme_gridelements_half_and_two_quarters.png', + ], + 'tx_eptheme_gridelements_two_quarters_and_half' => [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:ep_theme/Resources/Public/images/tx_eptheme_gridelements_two_quarters_and_half.png', + ], ]; diff --git a/packages/ep_theme/Configuration/TCA/Overrides/tt_content_element_container.php b/packages/ep_theme/Configuration/TCA/Overrides/tt_content_element_container.php index cb46c7b..5b4d438 100644 --- a/packages/ep_theme/Configuration/TCA/Overrides/tt_content_element_container.php +++ b/packages/ep_theme/Configuration/TCA/Overrides/tt_content_element_container.php @@ -31,6 +31,35 @@ call_user_func(function () { ) ); + // Column layouts, migrated from gridelements (ep:migrate-gridelements). colPos = 200 + 10 × layout + column + $menus = 'menu_abstract,menu_categorized_content,menu_categorized_pages,menu_recently_updated,menu_related_pages,menu_section_pages,menu_sitemap,menu_sitemap_pages'; + $columnContainers = [ + 'ep_container_halves' => ['Spalten 50/50', 'Zwei gleich breite Spalten', 'halves', [[201], [202]]], + 'ep_container_thirds' => ['Spalten 33/33/33', 'Drei gleich breite Spalten', 'thirds', [[211], [212], [213]]], + 'ep_container_columns_quarters' => ['Spalten 25/25/25/25', 'Vier gleich breite Spalten', 'quarters', [[221], [222], [223], [224]]], + 'ep_container_half_and_two_quarters' => ['Spalten 50/25/25', 'Eine breite und zwei schmale Spalten', 'half_and_two_quarters', [[231, 'textmedia,teaser,list,form_formframework'], [232], [233]]], + 'ep_container_two_quarters_and_half' => ['Spalten 25/25/50', 'Zwei schmale und eine breite Spalte', 'two_quarters_and_half', [[241], [242], [243, 'textmedia,teaser,list,gmap,nlform,form_formframework,menu_pages,menu_subpages,menu_section']]], + ]; + // No container inside these columns (topLevelLayout of the gridelements configuration) + $containers = implode(',', array_merge(['ep_container_quarters', 'ep_container_teasergroup'], array_keys($columnContainers))); + foreach ($columnContainers as $cType => [$label, $description, $icon, $columns]) { + $row = []; + foreach ($columns as $number => $column) { + $config = ['name' => 'Spalte ' . ($number + 1), 'colPos' => $column[0]]; + if (isset($column[1])) { + $config['colspan'] = 2; + $config['allowed'] = ['CType' => $column[1]]; + } else { + $config['disallowed'] = ['CType' => $menus . ',' . $containers]; + } + $row[] = $config; + } + GeneralUtility::makeInstance(Registry::class)->configureContainer( + (new ContainerConfiguration($cType, $label, $description, [$row])) + ->setIcon('tx_eptheme_gridelements_' . $icon) + ); + } + $headerConfig = [ 'showitem' => ' --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.general, @@ -47,7 +76,7 @@ call_user_func(function () { ' ]; - foreach (['ep_container_quarters', 'ep_container_teasergroup'] as $field) { + foreach (array_merge(['ep_container_quarters', 'ep_container_teasergroup'], array_keys($columnContainers)) as $field) { $GLOBALS['TCA']['tt_content']['types'][$field] = array_replace_recursive( $GLOBALS['TCA']['tt_content']['types'][$field], $headerConfig diff --git a/packages/ep_theme/Configuration/TSconfig/Page/Main.tsconfig b/packages/ep_theme/Configuration/TSconfig/Page/Main.tsconfig index e8fe637..c9a69d8 100644 --- a/packages/ep_theme/Configuration/TSconfig/Page/Main.tsconfig +++ b/packages/ep_theme/Configuration/TSconfig/Page/Main.tsconfig @@ -1,4 +1,3 @@ -@import 'EXT:ep_theme/Configuration/TSconfig/Page/Extensions/GridElements.tsconfig' @import 'EXT:ep_theme/Configuration/TSconfig/Page/Mod/SHARED.tsconfig' @import 'EXT:ep_theme/Configuration/TSconfig/Page/Mod/WebLayout/BackendLayouts.tsconfig' @import 'EXT:ep_theme/Configuration/TSconfig/Page/Mod/Wizards/NewContentElement.tsconfig' diff --git a/packages/ep_theme/Configuration/TSconfig/Page/TCEFORM.tsconfig b/packages/ep_theme/Configuration/TSconfig/Page/TCEFORM.tsconfig index bf95d78..4488eef 100644 --- a/packages/ep_theme/Configuration/TSconfig/Page/TCEFORM.tsconfig +++ b/packages/ep_theme/Configuration/TSconfig/Page/TCEFORM.tsconfig @@ -50,10 +50,14 @@ TCEFORM { 5 = Partnerlogos } } - gridelements_pi1 { + ep_container_halves { disabled = 0 removeItems := addToList(1,2,3) } + ep_container_thirds < .ep_container_halves + ep_container_columns_quarters < .ep_container_halves + ep_container_half_and_two_quarters < .ep_container_halves + ep_container_two_quarters_and_half < .ep_container_halves teaser { disabled = 0 removeItems := addToList(2,3) @@ -114,7 +118,11 @@ TCEFORM { space_before_class { types { - gridelements_pi1.disabled = 1 + ep_container_halves.disabled = 1 + ep_container_thirds.disabled = 1 + ep_container_columns_quarters.disabled = 1 + ep_container_half_and_two_quarters.disabled = 1 + ep_container_two_quarters_and_half.disabled = 1 form_formframework.disabled = 1 } removeItems := addToList(extra-small, medium, extra-large) diff --git a/packages/ep_theme/Configuration/TypoScript/ContentElements/container.typoscript b/packages/ep_theme/Configuration/TypoScript/ContentElements/container.typoscript index 4a07634..2df465c 100644 --- a/packages/ep_theme/Configuration/TypoScript/ContentElements/container.typoscript +++ b/packages/ep_theme/Configuration/TypoScript/ContentElements/container.typoscript @@ -1,3 +1,4 @@ +# Column layouts (formerly gridelements) tt_content.ep_container_halves =< lib.contentElement tt_content.ep_container_halves { templateName = Halves @@ -20,6 +21,40 @@ tt_content.ep_container_thirds { } } +tt_content.ep_container_columns_quarters =< lib.contentElement +tt_content.ep_container_columns_quarters { + templateName = ColumnsQuarters + templateRootPaths { + 10 = EXT:ep_theme/Resources/Private/Templates/Container + } + dataProcessing { + 100 = B13\Container\DataProcessing\ContainerProcessor + } +} + +tt_content.ep_container_half_and_two_quarters =< lib.contentElement +tt_content.ep_container_half_and_two_quarters { + templateName = HalfAndTwoQuarters + templateRootPaths { + 10 = EXT:ep_theme/Resources/Private/Templates/Container + } + dataProcessing { + 100 = B13\Container\DataProcessing\ContainerProcessor + } +} + +tt_content.ep_container_two_quarters_and_half =< lib.contentElement +tt_content.ep_container_two_quarters_and_half { + templateName = TwoQuartersAndHalf + templateRootPaths { + 10 = EXT:ep_theme/Resources/Private/Templates/Container + } + dataProcessing { + 100 = B13\Container\DataProcessing\ContainerProcessor + } +} + +# Teaser rows tt_content.ep_container_quarters =< lib.contentElement tt_content.ep_container_quarters { templateName = Quarters diff --git a/packages/ep_theme/Configuration/TypoScript/constants.typoscript b/packages/ep_theme/Configuration/TypoScript/constants.typoscript index db6d116..6171b9e 100644 --- a/packages/ep_theme/Configuration/TypoScript/constants.typoscript +++ b/packages/ep_theme/Configuration/TypoScript/constants.typoscript @@ -195,7 +195,6 @@ plugin.tx_eptheme { @import 'EXT:fluid_styled_content/Configuration/TypoScript/constants.typoscript' @import 'EXT:ep_products/Configuration/TypoScript/constants.typoscript' -@import 'EXT:gridelements/Configuration/TypoScript/constants.typoscript' @import 'EXT:seo/Configuration/TypoScript/XmlSitemap/constants.typoscript' @import 'EXT:typo3_encore/Configuration/TypoScript/constants.typoscript' @import 'EXT:news/Configuration/TypoScript/constants.txt' diff --git a/packages/ep_theme/Resources/Private/Templates/Container/ColumnsQuarters.html b/packages/ep_theme/Resources/Private/Templates/Container/ColumnsQuarters.html new file mode 100644 index 0000000..2657425 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Container/ColumnsQuarters.html @@ -0,0 +1,27 @@ + + + + + + + + + {f:render(section: 'Column', arguments: {children: children_221})} + {f:render(section: 'Column', arguments: {children: children_222})} + {f:render(section: 'Column', arguments: {children: children_223})} + {f:render(section: 'Column', arguments: {children: children_224})} + + + + + + + {record.renderedContent -> f:format.raw()} + + + + diff --git a/packages/ep_theme/Resources/Private/Templates/Container/HalfAndTwoQuarters.html b/packages/ep_theme/Resources/Private/Templates/Container/HalfAndTwoQuarters.html new file mode 100644 index 0000000..40bfca8 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Container/HalfAndTwoQuarters.html @@ -0,0 +1,36 @@ + + + + + + + + + + {f:render(section: 'Column', arguments: {children: children_231})} + + + + + {f:render(section: 'Column', arguments: {children: children_232})} + + + {f:render(section: 'Column', arguments: {children: children_233})} + + + + + + + + + + {record.renderedContent -> f:format.raw()} + + + + diff --git a/packages/ep_theme/Resources/Private/Templates/Container/Halves.html b/packages/ep_theme/Resources/Private/Templates/Container/Halves.html new file mode 100644 index 0000000..a78b595 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Container/Halves.html @@ -0,0 +1,35 @@ + + + + + + + + {f:render(section: 'Column', arguments: {children: children_201})} + {f:render(section: 'Column', arguments: {children: children_202})} + + + + {column1 -> f:format.raw()} + + + + + {column2 -> f:format.raw()} + + + + + + + + + {record.renderedContent -> f:format.raw()} + + + + diff --git a/packages/ep_theme/Resources/Private/Templates/Container/Thirds.html b/packages/ep_theme/Resources/Private/Templates/Container/Thirds.html new file mode 100644 index 0000000..a36fc50 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Container/Thirds.html @@ -0,0 +1,26 @@ + + + + + + + + + {f:render(section: 'Column', arguments: {children: children_211})} + {f:render(section: 'Column', arguments: {children: children_212})} + {f:render(section: 'Column', arguments: {children: children_213})} + + + + + + + {record.renderedContent -> f:format.raw()} + + + + diff --git a/packages/ep_theme/Resources/Private/Templates/Container/TwoQuartersAndHalf.html b/packages/ep_theme/Resources/Private/Templates/Container/TwoQuartersAndHalf.html new file mode 100644 index 0000000..0d56f63 --- /dev/null +++ b/packages/ep_theme/Resources/Private/Templates/Container/TwoQuartersAndHalf.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + {f:render(section: 'Column', arguments: {children: children_241})} + + + {f:render(section: 'Column', arguments: {children: children_242})} + + + + + {f:render(section: 'Column', arguments: {children: children_243})} + + + + + + + + {record.renderedContent -> f:format.raw()} + + + +