From 73e4298b753ec44dd17527a03f98791bd4a0f3d0 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Thu, 5 Nov 2026 10:59:56 +0100 Subject: [PATCH] Insights spotlight: news from folders 11 + 442, source editable in the menu - Megamenu entries with the catalog or spotlight layout get two editor fields, "Data source folders" (source_pages) and "Data source categories" (source_categories); the extension stores them, the site package's listener reads them from the entry row. - ProvideInsightsSpotlight reads its news from those folders (empty = 11 + 442; the newer posts live in 442, the query only read 11) and, when set, only from those categories (Insights: Blog Post + Press Release). Newest datetime first, three cards. - Pages carrying the menu are tagged with the news and event tables, so saving such a record refreshes the menu without a cache flush. - VITEC field descriptions via a TCA override; spec v1.31, README. Co-Authored-By: Claude Opus 5.5 --- .../TCA/tx_evomegamenujson_entry.php | 23 ++++- packages/evo_megamenu_json/README.md | 17 +++- .../Private/Language/locallang_db.xlf | 4 + packages/evo_megamenu_json/ext_tables.sql | 2 + .../ProvideInsightsSpotlight.php | 89 ++++++++++++++++--- .../Overrides/tx_evomegamenujson_entry.php | 18 ++++ .../Headless-JSON-Architecture.md | 7 +- 7 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 packages/vitec/Configuration/TCA/Overrides/tx_evomegamenujson_entry.php 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 d032e91..ca3c853 100644 --- a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php +++ b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php @@ -43,7 +43,7 @@ return [ '1' => [ 'showitem' => ' --div--;' . $ll . 'tab.general, - hidden, title, link, layout, tree_display, + hidden, title, link, layout, tree_display, source_pages, source_categories, --div--;' . $ll . 'tab.columns, menu_columns, --div--;' . $ll . 'tab.teasers, @@ -131,6 +131,27 @@ return [ ], ], ], + // data-driven layouts: where the site package's listener reads its + // records from - the extension only passes the entry row on + 'source_pages' => [ + 'displayCond' => 'FIELD:layout:IN:catalog,spotlight', + 'label' => $ll . 'source_pages', + 'description' => $ll . 'source_pages.description', + 'config' => [ + 'type' => 'group', + 'allowed' => 'pages', + 'size' => 3, + 'maxitems' => 20, + ], + ], + 'source_categories' => [ + 'displayCond' => 'FIELD:layout:IN:catalog,spotlight', + 'label' => $ll . 'source_categories', + 'description' => $ll . 'source_categories.description', + 'config' => [ + 'type' => 'category', + ], + ], 'menu_columns' => [ 'label' => $ll . 'menu_columns', diff --git a/packages/evo_megamenu_json/README.md b/packages/evo_megamenu_json/README.md index 8e6570e..500060b 100644 --- a/packages/evo_megamenu_json/README.md +++ b/packages/evo_megamenu_json/README.md @@ -117,6 +117,13 @@ final class ProvideCatalog `getEntry()` hands the listener the raw entry row (to tell several catalog entries apart), `getStage()` the content stage. +**Data source fields** (catalog and spotlight entries): *Data source +folders* (`source_pages`, comma list of page uids) and *Data source +categories* (`source_categories`, sys_category MM with +`tablenames = tx_evomegamenujson_entry`, `fieldname = source_categories`). +The extension only stores them; the listener reads them from the entry row +and decides what they mean (empty = its own default). + ### Layout `pagetree` A structure panel that can never age against the site: the columns mirror @@ -156,9 +163,13 @@ The VITEC site package ships `ProvideInsightsSpotlight`, which supplies } ``` -- `news`: the three latest news records (storage pid 11, type 0 - the Solr - convention), `link` = `/news/article/` per the NewsDetail - route enhancer, image from `fal_media` as processed 640px webp. +- `news`: the three latest news records (type 0 - the Solr convention) + from the entry's *Data source folders* (empty = 11 + 442) and, when set, + in one of its *Data source categories*; `link` = + `/news/article/` per the NewsDetail route enhancer, image + from `fal_media` as processed 640px webp. Pages carrying the menu are + tagged with the news and event tables, so saving such a record refreshes + the menu. - `events`: the next two events (`hideonwebsite` = 0, upcoming by eventstart/eventend), `link` = the record's `eventurl` or the events page, image processed the same way. diff --git a/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf b/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf index b1d63b2..b806927 100644 --- a/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf +++ b/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf @@ -27,6 +27,10 @@ Catalog (data-driven: categories and items from the site package, teaser cards as featured tiles) Page tree (data-driven: one column per child of the linked page, its children as items; curated columns are ignored) Spotlight (data-driven: left-rail nav from the linked page's children plus live visual cards - latest news, next events - supplied by the site package; curated columns are ignored) + Data source folders + For data-driven panels: the folders the site package reads its records from (e.g. news folders). Empty = the site package's default. + Data source categories + For data-driven panels: only records in one of these categories. Empty = no category restriction. Page tree display Columns: one column per child page with its children as links. List + cards: the child pages as a list on the left, the children of the hovered one as image cards on the right (images from the page media). Columns side by side diff --git a/packages/evo_megamenu_json/ext_tables.sql b/packages/evo_megamenu_json/ext_tables.sql index 1c6479e..2102139 100644 --- a/packages/evo_megamenu_json/ext_tables.sql +++ b/packages/evo_megamenu_json/ext_tables.sql @@ -9,6 +9,8 @@ CREATE TABLE tx_evomegamenujson_entry ( link varchar(1024) DEFAULT '' NOT NULL, layout varchar(30) DEFAULT 'columns' NOT NULL, tree_display varchar(30) DEFAULT 'columns' NOT NULL, + source_pages text, + source_categories int(11) unsigned DEFAULT '0' NOT NULL, menu_columns int(11) DEFAULT '0' NOT NULL, menu_teasers int(11) DEFAULT '0' NOT NULL, featured_enable tinyint(1) unsigned DEFAULT '0' NOT NULL, diff --git a/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php b/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php index 9596d58..e6df822 100644 --- a/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php +++ b/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php @@ -4,9 +4,12 @@ declare(strict_types=1); namespace Evomedien\Vitec\EventListener; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\ParameterType; use Evomedien\EvoMegamenuJson\Event\BuildSpotlightEvent; use TYPO3\CMS\Core\Attribute\AsEventListener; +use TYPO3\CMS\Core\Cache\CacheDataCollectorInterface; +use TYPO3\CMS\Core\Cache\CacheTag; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer; use TYPO3\CMS\Core\Resource\FileRepository; @@ -19,17 +22,23 @@ use TYPO3\CMS\Extbase\Service\ImageService; * news and the next events as image cards, queried live - editors touch * news and event records, never the menu. * - * Conventions mirrored from the site set: news live on pid 11 (type 0 only, - * same filter as the Solr indexing TS), their details render on page 21 - * through the NewsDetail route enhancer (/news/article/). + * Conventions mirrored from the site set: news are type 0 only (same filter + * as the Solr indexing TS), their details render on page 21 through the + * NewsDetail route enhancer (/news/article/). * Events come from the vitec event table; an event links to its own * eventurl when set, otherwise to the events page. + * + * News sources (2026-11-05, ticket 0046-FE: the newer posts sit in folder + * 442, the query only read 11): the entry's "Data source folders" and + * "Data source categories" decide, editable in the menu record. No folder + * set = 11 + 442 (like the /insights/news plugin), no category = all. */ #[AsEventListener(identifier: 'vitec/megamenu-insights-spotlight')] final class ProvideInsightsSpotlight { private const NEWS_TABLE = 'tx_news_domain_model_news'; - private const NEWS_STORAGE_PID = 11; + private const DEFAULT_NEWS_PIDS = [11, 442]; + private const ENTRY_TABLE = 'tx_evomegamenujson_entry'; private const NEWS_DETAIL_PATH = '/news/article/'; private const EVENT_TABLE = 'tx_vitec_domain_model_event'; private const EVENTS_PAGE = '/insights/events'; @@ -44,25 +53,83 @@ final class ProvideInsightsSpotlight public function __invoke(BuildSpotlightEvent $event): void { $stage = $event->getStage(); + $entry = $event->getEntry(); + // group field: "11,442" (older notation "pages_11" tolerated) + $pids = array_values(array_filter(array_map( + static fn(string $value): int => (int)preg_replace('/^\D+_/', '', trim($value)), + explode(',', (string)($entry['source_pages'] ?? '')) + ))); $event->setSpotlight([ - 'news' => $this->latestNews($stage), + 'news' => $this->latestNews($stage, $pids ?: self::DEFAULT_NEWS_PIDS, $this->entryCategories((int)($entry['uid'] ?? 0))), 'events' => $this->upcomingEvents($stage), ]); + $this->tagPageCache(); } - /** @return array> */ - private function latestNews(int $stage): array + /** + * The menu sits in every cached page; tagging the page with the news and + * event tables lets the DataHandler refresh those pages as soon as an + * editor saves a news or event record (it flushes the tag ``). + */ + private function tagPageCache(): void + { + try { + $collector = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('frontend.cache.collector'); + if ($collector instanceof CacheDataCollectorInterface) { + $collector->addCacheTags(new CacheTag(self::NEWS_TABLE), new CacheTag(self::EVENT_TABLE)); + } + } catch (\Throwable) { + // untagged, the menu only ages until the next flush + } + } + + /** + * Categories picked in the entry's "Data source categories". + * + * @return int[] + */ + private function entryCategories(int $entryUid): array + { + if ($entryUid <= 0) { + return []; + } + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_category_record_mm'); + return array_map('intval', $queryBuilder->select('uid_local')->from('sys_category_record_mm') + ->where( + $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter(self::ENTRY_TABLE)), + $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('source_categories')), + $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($entryUid, ParameterType::INTEGER)) + ) + ->executeQuery()->fetchFirstColumn()); + } + + /** + * @param int[] $pids news folders + * @param int[] $categories only news in one of these; empty = all + * @return array> + */ + private function latestNews(int $stage, array $pids, array $categories): array { $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::NEWS_TABLE); $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); try { - $rows = $queryBuilder->select('uid', 'title', 'teaser', 'datetime', 'path_segment') + $queryBuilder->select('uid', 'title', 'teaser', 'datetime', 'path_segment') ->from(self::NEWS_TABLE) ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(self::NEWS_STORAGE_PID, ParameterType::INTEGER)), + $queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($pids, ArrayParameterType::INTEGER)), $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter('0')) - ) - ->orderBy('datetime', 'DESC') + ); + if ($categories !== []) { + $mm = $this->connectionPool->getQueryBuilderForTable('sys_category_record_mm'); + $mm->select('uid_foreign')->from('sys_category_record_mm') + ->where( + $mm->expr()->eq('tablenames', $queryBuilder->createNamedParameter(self::NEWS_TABLE)), + $mm->expr()->eq('fieldname', $queryBuilder->createNamedParameter('categories')), + $mm->expr()->in('uid_local', $queryBuilder->createNamedParameter($categories, ArrayParameterType::INTEGER)) + ); + $queryBuilder->andWhere($queryBuilder->expr()->in('uid', $mm->getSQL())); + } + $rows = $queryBuilder->orderBy('datetime', 'DESC') ->setMaxResults(self::NEWS_COUNT) ->executeQuery()->fetchAllAssociative(); } catch (\Throwable) { diff --git a/packages/vitec/Configuration/TCA/Overrides/tx_evomegamenujson_entry.php b/packages/vitec/Configuration/TCA/Overrides/tx_evomegamenujson_entry.php new file mode 100644 index 0000000..8b873a4 --- /dev/null +++ b/packages/vitec/Configuration/TCA/Overrides/tx_evomegamenujson_entry.php @@ -0,0 +1,18 @@ +` grouped by sysfolders (3 categories → 9 groups) below /products. The megamenu `catalog` payload is now **version 2**: `categories[].groups[].items[]` of type `page` (overview page with `productCount`/`products`/`image`) or `shortcut` (`target` indices), plus `viewAll` and `news`; the menu no longer lists products. Transitional v1 fields (`groups[].products` etc.) stay until the front end has switched. The product type-ahead (`?type=7385`) gained `overviews` and a per-product `overview` with menu path. | | 1.29 | 2026‑11‑02 | **Interface change (removal within the unreleased catalog v2).** `catalog.news` dropped (decision: no automatic news in the products menu). Both promo boxes of the products panel, "Product highlight" and "Product news", are the entry's editorial `teasers` (`kicker` = box label). Megamenu caching: pages carrying the menu are now tagged with the menu tables, so saving menu records refreshes every page; data read from the page tree still needs a cache flush. | | 1.30 | 2026‑11‑02 | **Interface change (additive).** Megamenu `pagetree` entries carry `display` (`columns` \| `rail`) and, for `rail`, `cardSize` (`regular` \| `compact`): Markets and Solutions switch to a left list with the sub-pages as image cards on hover. Page-tree items gained `image` (page media) and `teaser` (abstract) from stage 2; the sub-solution pages received their hero background as page media (sub-markets had page media already). | +| 1.31 | 2026‑11‑05 | **No interface change (data source).** Megamenu Insights `spotlight.news` read only news folder 11; the newer posts live in 442 (ticket 0046-FE). Catalog and spotlight entries gained the editor fields *Data source folders* and *Data source categories*; the Insights listener reads news from them (empty = 11 + 442, no category filter), newest `datetime` first, three cards. Pages carrying the menu are tagged with the news and event tables, so a saved news or event record refreshes the menu. Contract unchanged. | This document is drafted in the style of, and adopts the terminology conventions of, ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for @@ -1705,4 +1706,4 @@ remediation. - Header convention — the uniform header section across CEs, plugins and containers. - `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point. -*End of document EVO‑VITEC‑HL‑001 v1.30.* +*End of document EVO‑VITEC‑HL‑001 v1.31.*