Products megamenu as a data-driven catalog with a Solr product type-ahead

- evo_megamenu_json stays project-neutral: new panel layout "catalog";
  for such entries the MenuBuilder dispatches the new PSR-14 event
  BuildCatalogEvent and emits whatever a listener sets as `catalog`
  (null without a listener). The entry's teaser cards serve as the
  featured tiles; the teaser inline field now also shows for "catalog",
  and the entry form reloads when the layout changes. README section
  "Layout catalog"
- VITEC listener ProvideProductCatalog fills it, modelled on the Ross
  Video products menu: main categories below the "Product" root, their
  subcategories as groups, products with a plain-text one-line teaser;
  same visibility rules as the product list, Product Finder links
  (/products?cat=&subcat=), empty categories and groups left out, three
  queries for the whole catalog
- Product type-ahead GET /_json/?type=7385&q= (USER_INT page type,
  ProductMenuSearchJsonRenderer): Solr restricted to products, last term
  completed as a prefix, up to six product cards plus their titles as
  autocomplete suggestions; endpoint announced in catalog.search
- Spec v1.23 (clause 7.16: product type-ahead)
This commit is contained in:
orasche committed 2026-10-09 13:40:23 +02:00
1 parent b5fcdb2b5a
commit 887caed2aa
8 files changed
+502 -6

No files matched your search

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Evomedien\EvoMegamenuJson\Service;
use Doctrine\DBAL\ParameterType;
use Evomedien\EvoMegamenuJson\Event\BuildCatalogEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
@@ -52,6 +54,7 @@ final class MenuBuilder
private readonly FlexFormService $flexFormService,
private readonly PageRepository $pageRepository,
private readonly FileRepository $fileRepository,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
/**
@@ -82,7 +85,10 @@ final class MenuBuilder
*/
private function buildEntry(array $entry, int $stage, ContentObjectRenderer $cObj): array
{
$layout = (string)($entry['layout'] ?? 'columns') === 'teasers' ? 'teasers' : 'columns';
$layout = (string)($entry['layout'] ?? 'columns');
if (!in_array($layout, ['columns', 'teasers', 'catalog'], true)) {
$layout = 'columns';
}
$columns = [];
foreach ($this->children(self::TABLE_COLUMN, (int)$entry['uid']) as $column) {
@@ -104,7 +110,15 @@ final class MenuBuilder
'columns' => $columns,
];
if ($layout === 'teasers') {
// Catalog panels: the teaser cards serve as the featured tiles (left
// column), the catalog data itself comes from a listener of
// BuildCatalogEvent - this extension does not know the data source.
if ($layout === 'catalog') {
$event = $this->eventDispatcher->dispatch(new BuildCatalogEvent($entry, $stage));
$built['catalog'] = $event->getCatalog();
}
if ($layout === 'teasers' || $layout === 'catalog') {
$teasers = [];
foreach ($this->children(self::TABLE_TEASER, (int)$entry['uid']) as $teaser) {
$card = [