From 8497dad06aee2fde1279a82d75dd9630a78f2301 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Tue, 27 Oct 2026 12:08:26 +0100 Subject: [PATCH] Megamenu follow-ups: nav_hide honoured, entries insertable, page tiles - getMenu() treats hidden/time/fe_group as enable fields but NOT nav_hide - the treeColumns doc comment claimed otherwise, and the news detail page ("article") stayed in the new spotlight rail after the editor hid it in menus. All three getMenu() calls (spotlight nav, pagetree columns and their items) now pass 'AND nav_hide = 0'; verified live (6 rail items) - all four tx_evomegamenujson_* tables get security.ignorePageTypeRestriction: they are inline children of the menu plugin and live on whatever page the plugin sits on - creating a new entry on the standard page failed with "table is not allowed" - new vitec:attach-page-media --root= (dry-run by default): attaches the numbered placeholder pages/-08.png (slot 08 = page media / menu tile, the number doubles as the image wish list) as page media to every child page without one; existing media is never touched. Run for /support (8 pages) so the planned Support menu entry renders as two 4-tile rows via the pagetree layout's column images Co-Authored-By: Claude Fable 5 --- .../Classes/Service/MenuBuilder.php | 19 ++- .../TCA/tx_evomegamenujson_column.php | 5 + .../TCA/tx_evomegamenujson_entry.php | 5 + .../TCA/tx_evomegamenujson_item.php | 5 + .../TCA/tx_evomegamenujson_teaser.php | 5 + .../Command/AttachPageMediaCommand.php | 121 ++++++++++++++++++ 6 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 packages/vitec/Classes/Command/AttachPageMediaCommand.php diff --git a/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php b/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php index 5708b35..6879920 100644 --- a/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php +++ b/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php @@ -39,6 +39,9 @@ final class MenuBuilder private const TABLE_ITEM = 'tx_evomegamenujson_item'; private const TABLE_TEASER = 'tx_evomegamenujson_teaser'; + /** getMenu() does not filter "hide in menus" by itself. */ + private const NAV_WHERE = 'AND nav_hide = 0'; + private const DEFAULT_SETTINGS = [ 'stage' => 3, 'pendingBehaviour' => 'fallback', @@ -208,8 +211,11 @@ final class MenuBuilder /** * Columns from the page tree below the entry's linked page. getMenu() - * applies the frontend restrictions (hidden, time, nav_hide, fe_group), - * so an editor hides a page and it leaves the menu with the next flush. + * applies the frontend restrictions (hidden, time, fe_group); nav_hide + * is NOT an enable field and goes through the additional where clause + * (found 2026-10-27 - the news detail page stayed in the spotlight + * rail). An editor hides a page and it leaves the menu with the next + * cache flush. * * @return array> */ @@ -220,9 +226,9 @@ final class MenuBuilder return []; } $columns = []; - foreach ($this->pageRepository->getMenu($rootUid) as $child) { + foreach ($this->pageRepository->getMenu($rootUid, '*', 'sorting', self::NAV_WHERE) as $child) { $items = []; - foreach ($this->pageRepository->getMenu((int)$child['uid']) as $grandchild) { + foreach ($this->pageRepository->getMenu((int)$child['uid'], '*', 'sorting', self::NAV_WHERE) as $grandchild) { $items[] = [ 'title' => (string)(($grandchild['nav_title'] ?? '') !== '' ? $grandchild['nav_title'] : $grandchild['title']), 'link' => $this->url('t3://page?uid=' . (int)$grandchild['uid'], $cObj), @@ -250,8 +256,7 @@ final class MenuBuilder /** * Flat left-rail nav for the spotlight layout: the linked page's children * with abstract and page media, so the front end can render the rail and - * swap the visual area on hover. Same visibility rules as treeColumns - * (getMenu applies hidden, time, nav_hide and fe_group). + * swap the visual area on hover. Same visibility rules as treeColumns. * * @return array> */ @@ -262,7 +267,7 @@ final class MenuBuilder return []; } $items = []; - foreach ($this->pageRepository->getMenu($rootUid) as $child) { + foreach ($this->pageRepository->getMenu($rootUid, '*', 'sorting', self::NAV_WHERE) as $child) { $item = [ 'title' => (string)(($child['nav_title'] ?? '') !== '' ? $child['nav_title'] : $child['title']), 'link' => $this->url('t3://page?uid=' . (int)$child['uid'], $cObj), diff --git a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_column.php b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_column.php index 914689f..df0979d 100644 --- a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_column.php +++ b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_column.php @@ -18,6 +18,11 @@ return [ 'delete' => 'deleted', 'sortby' => 'sorting', 'hideTable' => true, + 'security' => [ + // inline children of the menu plugin - they live on whatever + // page the plugin sits on, not only on sysfolders + 'ignorePageTypeRestriction' => true, + ], 'versioningWS' => true, 'languageField' => 'sys_language_uid', 'transOrigPointerField' => 'l10n_parent', diff --git a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php index f41d582..5b25977 100644 --- a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php +++ b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php @@ -24,6 +24,11 @@ return [ 'delete' => 'deleted', 'sortby' => 'sorting', 'hideTable' => true, + 'security' => [ + // inline children of the menu plugin - they live on whatever + // page the plugin sits on, not only on sysfolders + 'ignorePageTypeRestriction' => true, + ], 'versioningWS' => true, 'languageField' => 'sys_language_uid', 'transOrigPointerField' => 'l10n_parent', diff --git a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_item.php b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_item.php index 386ba87..a4ee659 100644 --- a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_item.php +++ b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_item.php @@ -21,6 +21,11 @@ return [ 'delete' => 'deleted', 'sortby' => 'sorting', 'hideTable' => true, + 'security' => [ + // inline children of the menu plugin - they live on whatever + // page the plugin sits on, not only on sysfolders + 'ignorePageTypeRestriction' => true, + ], 'versioningWS' => true, 'languageField' => 'sys_language_uid', 'transOrigPointerField' => 'l10n_parent', diff --git a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_teaser.php b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_teaser.php index 6c4f1d5..38bee70 100644 --- a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_teaser.php +++ b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_teaser.php @@ -19,6 +19,11 @@ return [ 'delete' => 'deleted', 'sortby' => 'sorting', 'hideTable' => true, + 'security' => [ + // inline children of the menu plugin - they live on whatever + // page the plugin sits on, not only on sysfolders + 'ignorePageTypeRestriction' => true, + ], 'versioningWS' => true, 'languageField' => 'sys_language_uid', 'transOrigPointerField' => 'l10n_parent', diff --git a/packages/vitec/Classes/Command/AttachPageMediaCommand.php b/packages/vitec/Classes/Command/AttachPageMediaCommand.php new file mode 100644 index 0000000..6bd6ce3 --- /dev/null +++ b/packages/vitec/Classes/Command/AttachPageMediaCommand.php @@ -0,0 +1,121 @@ +-08.png` when it exists (slot 08 = + * page media / menu tile; the number in the picture doubles as the image + * wish list towards the agency), otherwise the generic wide grey + * placeholder. A page that already has a media reference is NEVER touched - + * editors swap the placeholder for the real image in the page properties. + * + * vendor/bin/typo3 vitec:attach-page-media --root=/support + * vendor/bin/typo3 vitec:attach-page-media --root=/support --apply + */ +#[AsCommand( + name: 'vitec:attach-page-media', + description: 'Attach placeholder page media to the children of a root page (dry-run by default)' +)] +final class AttachPageMediaCommand extends Command +{ + protected function configure(): void + { + $this->addOption('root', null, InputOption::VALUE_REQUIRED, 'Slug of the root page, e.g. /support'); + $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Bootstrap::initializeBackendAuthentication(); + $apply = (bool)$input->getOption('apply'); + $rootSlug = '/' . trim((string)$input->getOption('root'), '/ '); + $pool = GeneralUtility::makeInstance(ConnectionPool::class); + $pages = $pool->getConnectionForTable('pages'); + + $root = $pages->fetchAssociative('SELECT uid, title FROM pages WHERE deleted = 0 AND slug = ? AND sys_language_uid IN (0,-1)', [$rootSlug]); + if ($root === null || $root === false) { + $output->writeln('Root page ' . $rootSlug . ' not found.'); + return Command::FAILURE; + } + $children = $pages->fetchAllAssociative( + 'SELECT uid, title FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype = 1 AND pid = ? AND sys_language_uid IN (0,-1) ORDER BY sorting', + [(int)$root['uid']] + ); + $output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · %d child page(s)', $rootSlug, (int)$root['uid'], count($children))); + + $storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage(); + $refs = $pool->getConnectionForTable('sys_file_reference'); + $datamap = []; + $newIndex = 0; + foreach ($children as $child) { + $uid = (int)$child['uid']; + $existing = (int)$refs->fetchOne( + "SELECT COUNT(*) FROM sys_file_reference WHERE deleted = 0 AND tablenames = 'pages' AND fieldname = 'media' AND uid_foreign = ?", + [$uid] + ); + if ($existing > 0) { + $output->writeln(sprintf(' = #%d "%s": has media (%d) - untouched', $uid, (string)$child['title'], $existing)); + continue; + } + $identifier = 'placeholders/pages/' . $uid . '-08.png'; + if (!$storage->hasFile($identifier)) { + $identifier = 'placeholders/placeholder-grey-wide.png'; + if (!$storage->hasFile($identifier)) { + $output->writeln(sprintf(' ! #%d "%s": no placeholder file - skipped', $uid, (string)$child['title'])); + continue; + } + } + $output->writeln(sprintf(' + #%d "%s": attach %s', $uid, (string)$child['title'], $identifier)); + if (!$apply) { + continue; + } + $file = $storage->getFile($identifier); + $datamap['sys_file_reference']['NEWPM' . (++$newIndex)] = [ + 'pid' => $uid, + 'table_local' => 'sys_file', + 'uid_local' => $file->getUid(), + 'tablenames' => 'pages', + 'uid_foreign' => $uid, + 'fieldname' => 'media', + ]; + $datamap['pages'][$uid] = ['media' => 1]; + } + + if (!$apply) { + $output->writeln("\nDRY RUN - nothing written. Add --apply to write."); + return Command::SUCCESS; + } + if ($datamap === []) { + $output->writeln('Nothing to attach.'); + return Command::SUCCESS; + } + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start($datamap, []); + $dataHandler->process_datamap(); + if ($dataHandler->errorLog !== []) { + $output->writeln('DataHandler: ' . implode(' | ', $dataHandler->errorLog) . ''); + return Command::FAILURE; + } + $output->writeln('Done. Flush the frontend cache; the menu picks the images up.'); + return Command::SUCCESS; + } +}