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:
1 parent
b5fcdb2b5a
commit
887caed2aa
8 files changed
+502
-6
No files matched your search
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\EvoMegamenuJson\Event;
|
||||
|
||||
/**
|
||||
* Dispatched for every entry with the panel layout `catalog`.
|
||||
*
|
||||
* The extension stays project-neutral: it knows that a catalog panel exists,
|
||||
* not where its data comes from. A site package listens to this event and
|
||||
* supplies the data (categories, their groups and items, a search endpoint,
|
||||
* ...) as a plain array; it is emitted verbatim as `catalog` on the entry.
|
||||
* Without a listener the entry carries `catalog: null`.
|
||||
*/
|
||||
final class BuildCatalogEvent
|
||||
{
|
||||
/** @var array<string,mixed>|null */
|
||||
private ?array $catalog = null;
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $entry the raw tx_evomegamenujson_entry row
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $entry,
|
||||
private readonly int $stage,
|
||||
) {}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function getEntry(): array
|
||||
{
|
||||
return $this->entry;
|
||||
}
|
||||
|
||||
/** Content stage of the menu (1 structure, 2 + images/teasers, 3 + featured). */
|
||||
public function getStage(): int
|
||||
{
|
||||
return $this->stage;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function getCatalog(): ?array
|
||||
{
|
||||
return $this->catalog;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $catalog */
|
||||
public function setCatalog(array $catalog): void
|
||||
{
|
||||
$this->catalog = $catalog;
|
||||
}
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
@@ -17,9 +17,10 @@ A plugin called **Megamenu**. Inside it:
|
||||
| **Teaser card** | kicker, headline, text, link, image — for editorial panels |
|
||||
| **Featured** | one highlighted block per entry, edited on the entry itself |
|
||||
|
||||
Two panel layouts per entry: `columns` for a structure menu (products,
|
||||
solutions, markets) and `teasers` for a small link column next to cards
|
||||
(news, stories).
|
||||
Three panel layouts per entry: `columns` for a structure menu (products,
|
||||
solutions, markets), `teasers` for a small link column next to cards
|
||||
(news, stories), and `catalog` for a data-driven panel whose content comes
|
||||
from the site package (see below).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -89,6 +90,31 @@ An item whose target page does not exist yet carries `"pending": true`.
|
||||
column heading instead), `mute` (show, not clickable) or `hide`. This keeps
|
||||
a menu shippable while its deeper pages are still being built.
|
||||
|
||||
### Layout `catalog`
|
||||
|
||||
For panels built from data rather than by hand — a product catalog with
|
||||
categories, groups and items, a search bar, whatever the site needs. The
|
||||
extension stays project-neutral: it dispatches
|
||||
`Evomedien\EvoMegamenuJson\Event\BuildCatalogEvent` for every catalog entry
|
||||
and emits whatever a listener sets via `setCatalog()` verbatim as
|
||||
`catalog` on the entry (`null` without a listener). The entry's teaser cards
|
||||
are emitted as `teasers` as usual and serve as the panel's featured tiles;
|
||||
its columns are ignored.
|
||||
|
||||
```php
|
||||
#[AsEventListener(identifier: 'site/megamenu-catalog')]
|
||||
final class ProvideCatalog
|
||||
{
|
||||
public function __invoke(BuildCatalogEvent $event): void
|
||||
{
|
||||
$event->setCatalog(['categories' => [/* … */]]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`getEntry()` hands the listener the raw entry row (to tell several catalog
|
||||
entries apart), `getStage()` the content stage.
|
||||
|
||||
## Notes
|
||||
|
||||
* Every link is a resolved URL — page, record, external or file, run through
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<trans-unit id="tx_evomegamenujson_entry.layout.description"><source>Columns for a structure menu, teaser cards for editorial sections such as news or stories.</source></trans-unit>
|
||||
<trans-unit id="tx_evomegamenujson_entry.layout.columns"><source>Columns</source></trans-unit>
|
||||
<trans-unit id="tx_evomegamenujson_entry.layout.teasers"><source>Link column plus teaser cards</source></trans-unit>
|
||||
<trans-unit id="tx_evomegamenujson_entry.layout.catalog"><source>Catalog (data-driven: categories and items from the site package, teaser cards as featured tiles)</source></trans-unit>
|
||||
<trans-unit id="tx_evomegamenujson_entry.menu_columns"><source>Columns</source></trans-unit>
|
||||
<trans-unit id="tx_evomegamenujson_entry.menu_columns.description"><source>One column per group. In the teaser layout only the first column is rendered, next to the cards.</source></trans-unit>
|
||||
<trans-unit id="tx_evomegamenujson_entry.menu_teasers"><source>Teaser cards</source></trans-unit>
|
||||
|
||||
Reference in new issue
Block a user