Show success stories below the product lists, linked on the category
sys_category gains a story selection and a layout (Options tab, alongside the images field EXT:news puts there), so one editorial decision serves every product list that renders the category. ProductListJsonRenderer emits them beside the products in the 7.3 list envelope; each card carries forCategories - the product categories it came from - so the front end hides it with the same ?cat=/?subcat= filter it applies to the products. Stories are collected over the category and its descendants and deduplicated. Story links follow the existing rule (detail page's parent path plus slug, which SuccessStoryPathRewrite maps back), taking the page from the element's FlexForm or the new site setting vitec.storyDetailPid. Spec raised to 1.17 with clause 7.15.2.1; the header had been left at 1.12 although entries up to 1.15 already existed, so today's two entries were renumbered to 1.16 and 1.17.
This commit is contained in:
@@ -204,6 +204,14 @@ class ProductListJsonRenderer
|
||||
'layout' => (string)($settings['layout'] ?? '0'),
|
||||
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||
'products' => $productsData,
|
||||
// Success stories the editor attached to these categories
|
||||
// (sys_category, Options tab). They ride along with the list
|
||||
// so the front end can hide them with the same category
|
||||
// filter it applies to the products.
|
||||
'successStories' => $this->successStoriesFor(
|
||||
$allProducts ? [] : $categoryUids,
|
||||
(int)($settings['storysinglepid'] ?? 0)
|
||||
),
|
||||
];
|
||||
|
||||
if ($debugMode) {
|
||||
@@ -222,6 +230,141 @@ class ProductListJsonRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Success stories attached to the given categories, in the order the
|
||||
* categories were resolved and, inside one category, in the order the
|
||||
* editor arranged them. A story linked from several of the categories
|
||||
* appears once and carries all of them in `forCategories`, which is what
|
||||
* the front end filters on: the product finder writes ?cat= / ?subcat=,
|
||||
* and a card stays visible while one of its categories is selected.
|
||||
*
|
||||
* Cards use the shared story shape (UsecaseSerializer), so the front end
|
||||
* can render them with the same component as every other story list.
|
||||
* `detailUrl` follows the same rule as the story list: the story's own
|
||||
* detail page wins, otherwise the Single PID configured on this element
|
||||
* plus the slug - and null when neither is available.
|
||||
*
|
||||
* Returned in the list envelope of 7.3 - `layout` plus the array - so the
|
||||
* front end treats these cards like every other story list. The layout is
|
||||
* an editorial setting on the category, next to the selection itself.
|
||||
*
|
||||
* @param int[] $categoryUids
|
||||
* @return array{layout:string,stories:array<int,array<string,mixed>>}
|
||||
*/
|
||||
private function successStoriesFor(array $categoryUids, int $singlePid): array
|
||||
{
|
||||
if ($categoryUids === []) {
|
||||
return ['layout' => 'grid', 'stories' => []];
|
||||
}
|
||||
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_category');
|
||||
$rows = $queryBuilder->select('uid', 'tx_vitec_success_stories', 'tx_vitec_success_stories_layout')->from('sys_category')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)),
|
||||
$queryBuilder->expr()->eq('deleted', 0),
|
||||
$queryBuilder->expr()->eq('hidden', 0)
|
||||
)->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$listByCategory = [];
|
||||
$layoutByCategory = [];
|
||||
foreach ($rows as $row) {
|
||||
$listByCategory[(int)$row['uid']] = (string)($row['tx_vitec_success_stories'] ?? '');
|
||||
$layoutByCategory[(int)$row['uid']] = (string)($row['tx_vitec_success_stories_layout'] ?? '') ?: 'grid';
|
||||
}
|
||||
|
||||
$order = [];
|
||||
$forCategories = [];
|
||||
$layout = 'grid';
|
||||
foreach ($categoryUids as $categoryUid) {
|
||||
foreach (explode(',', $listByCategory[(int)$categoryUid] ?? '') as $raw) {
|
||||
$storyUid = (int)trim($raw);
|
||||
if ($storyUid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($order === []) {
|
||||
// The first category that actually contributes stories sets
|
||||
// the layout - that is the top-most one the element is
|
||||
// configured with, which is where an editor expects the
|
||||
// setting to live.
|
||||
$layout = $layoutByCategory[(int)$categoryUid] ?? 'grid';
|
||||
}
|
||||
if (!in_array($storyUid, $order, true)) {
|
||||
$order[] = $storyUid;
|
||||
}
|
||||
$forCategories[$storyUid][] = (int)$categoryUid;
|
||||
}
|
||||
}
|
||||
if ($order === []) {
|
||||
return ['layout' => 'grid', 'stories' => []];
|
||||
}
|
||||
|
||||
$storyQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||
$storyRows = $storyQueryBuilder->select('*')->from('tx_vitec_domain_model_usecase')
|
||||
->where(
|
||||
$storyQueryBuilder->expr()->in('uid', $storyQueryBuilder->createNamedParameter($order, Connection::PARAM_INT_ARRAY)),
|
||||
$storyQueryBuilder->expr()->eq('deleted', 0),
|
||||
$storyQueryBuilder->expr()->eq('hidden', 0)
|
||||
)->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$byUid = [];
|
||||
foreach ($storyRows as $storyRow) {
|
||||
$byUid[(int)$storyRow['uid']] = $storyRow;
|
||||
}
|
||||
|
||||
// Same base as the story list: the detail page's PARENT path, because
|
||||
// SuccessStoryPathRewrite maps /success-stories/<slug> onto the detail
|
||||
// subpage at request time. Falls back to the site setting
|
||||
// `vitec.storyDetailPid`, so the editor does not have to configure the
|
||||
// page on every single product list.
|
||||
if ($singlePid <= 0) {
|
||||
$singlePid = $this->storyDetailPidFromSite();
|
||||
}
|
||||
$detailBase = '';
|
||||
if ($singlePid > 0) {
|
||||
$detailPath = rtrim(\Evomedien\Vitec\Service\LinkResolver::pageUrl($singlePid) ?? '', '/');
|
||||
$parent = str_contains($detailPath, '/') ? substr($detailPath, 0, (int)strrpos($detailPath, '/')) : '';
|
||||
$detailBase = $parent !== '' ? $parent : $detailPath;
|
||||
}
|
||||
|
||||
$serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class);
|
||||
$stories = [];
|
||||
foreach ($order as $storyUid) {
|
||||
if (!isset($byUid[$storyUid])) {
|
||||
continue; // hidden or deleted meanwhile - simply drops out
|
||||
}
|
||||
$item = $serializer->serializeListItem($byUid[$storyUid]);
|
||||
if (($item['detailUrl'] ?? null) === null && $detailBase !== '' && ($item['slug'] ?? '') !== '') {
|
||||
$item['detailUrl'] = $detailBase . '/' . ltrim((string)$item['slug'], '/');
|
||||
}
|
||||
// NOT the story's own topic categories (those stay in `categories`)
|
||||
// but the product categories it was attached to.
|
||||
$item['forCategories'] = array_values(array_unique($forCategories[$storyUid]));
|
||||
$stories[] = $item;
|
||||
}
|
||||
|
||||
return ['layout' => $layout, 'stories' => $stories];
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide fallback for the success story detail page, 0 when unset.
|
||||
* Read from the site settings rather than hard-coded, so the page can be
|
||||
* moved without touching code.
|
||||
*/
|
||||
private function storyDetailPidFromSite(): int
|
||||
{
|
||||
try {
|
||||
$site = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('site');
|
||||
if ($site === null || !method_exists($site, 'getSettings')) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$site->getSettings()->get('vitec.storyDetailPid', 0);
|
||||
} catch (\Throwable) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The given category uids plus every descendant category uid, DEPTH first
|
||||
* over sys_category.parent with siblings in sys_category.sorting order -
|
||||
|
||||
Reference in New Issue
Block a user