From 4a7ea387a0c5610fc23c433f337ad7cc082a0159 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Wed, 16 Sep 2026 14:43:00 +0200 Subject: [PATCH] Product list: success stories on the all-products element The big product list on /products (all products, no category context) deliberately delivered the empty successStories envelope; the front end now renders story cards there too and filters them client-side, so the element aggregates over the WHOLE product category tree instead: - new allProductCategoryUids(): root category 'Product' (same detection as ProductFinderJsonRenderer), children plus descendants depth-first, fed through the existing successStoriesFor() - per-category elements are untouched; forCategories keeps carrying the category uids a story is attached to, so the ?cat=/?subcat= filter behaves identically on both element kinds - layout rule unchanged: first contributing category in tree order Co-Authored-By: Claude Fable 5 --- .../UserFunc/ProductListJsonRenderer.php | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php index ff1a0de..0f20425 100755 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -207,9 +207,13 @@ class ProductListJsonRenderer // 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. + // filter it applies to the products. An all-products element + // aggregates over the WHOLE product category tree (change + // 2026-09-16 on front-end request - it used to deliver the + // empty envelope), so forCategories keeps driving the + // ?cat=/?subcat= filter exactly like on per-category lists. 'successStories' => $this->successStoriesFor( - $allProducts ? [] : $categoryUids, + ($allProducts || $categoryUids === []) ? $this->allProductCategoryUids() : $categoryUids, (int)($settings['storysinglepid'] ?? 0) ), ]; @@ -375,6 +379,34 @@ class ProductListJsonRenderer * @param int[] $categoryUids * @return int[] */ + /** + * The whole product category tree - the root's children plus all + * descendants in depth-first backend order. Story source for elements + * that render all products. Root detection mirrors + * ProductFinderJsonRenderer: the root-level sys_category named 'Product'. + * + * @return int[] + */ + private function allProductCategoryUids(): array + { + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('sys_category'); + $rootUid = (int)($queryBuilder->select('uid')->from('sys_category') + ->where( + $queryBuilder->expr()->eq('parent', 0), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter('Product', ParameterType::STRING)) + ) + ->setMaxResults(1) + ->executeQuery()->fetchOne() ?: 0); + if ($rootUid <= 0) { + return []; + } + + // Drop the root itself - stories hang on the categories below it. + return array_values(array_diff($this->expandWithDescendants([$rootUid]), [$rootUid])); + } + private function expandWithDescendants(array $categoryUids): array { $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)