Product texts: mapping completed; product model: solutions + resources
Two work packages, one deploy.
Workbook field mapping completed (2026-09-18)
- highlights <- g:Technical Highlight (new spec-row component of the
full agency delivery; composeGroup() is generic, no change needed)
- applications <- g:Why Choose Card (benefit cards as one block)
- seometa <- new composed source x:seometa (Introduction body trimmed
at a word boundary to ~155 chars - the SEO sheet ships no meta
description, a trimmed intro beats an empty tag)
- teaser stays the hero one-liner, settling the varchar(255) question
- fix: "Save mapping" now merges over the stored mapping instead of
overwriting it - an empty dropdown no longer wipes a stored selector
Product model: solutions relation and Resources tab (2026-09-24)
- closes the gaps the full workbook walkthrough exposed (rows the
import deliberately skips because the model had no place for them)
- solutions: MM multi select to tx_vitec_domain_model_solution
(tx_vitec_product_solution_mm, relatedprodukt pattern), emitted by
both product renderers as solution cards in the shared shape
- Resources tab: inline child table tx_vitec_product_resource - per
entry a type (documentation/support/datasheet), editable card title
and text, an icon (free text with a value-picker seed set) and one
link (page/url/file/record via the link wizard, so the Download tab
works too). Deliberately no Extbase model: only the UserFunc path
reads the rows.
- JSON `resources` (detail payload only, like heroimage): list envelope
{layout, items[]}; a card whose link is empty or unresolvable is not
emitted; layout switches to carousel above three cards
- spec raised to v1.18 (7.15 member table, revision row; the footer had
been stuck at v1.15 since 1.16/1.17)
Requires on the server:
vendor/bin/typo3 database:updateschema "*.add,*.change"
vendor/bin/typo3 cache:flush
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a7ea387a0
commit
3df66b1353
9 files changed
+394
-4
No files matched your search
@@ -488,6 +488,7 @@ class ProductListJsonRenderer
|
||||
'ogimage' => $this->getProductOgImage($uid),
|
||||
'videofile' => $this->getProductVideoFile($uid),
|
||||
'relatedprodukt' => $this->getRelatedProducts($uid),
|
||||
'solutions' => $this->getProductSolutions($uid),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -911,6 +912,48 @@ class ProductListJsonRenderer
|
||||
return $fallbackFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solutions linked to this product (tx_vitec_product_solution_mm, editor
|
||||
* order) in the shared solution card shape - the same fields the solution
|
||||
* list emits, so the front end reuses its card component. detailUrl is
|
||||
* null while a solution has no detail page (9.12 contract).
|
||||
*/
|
||||
protected function getProductSolutions(int $productUid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_vitec_domain_model_solution');
|
||||
$rows = $queryBuilder
|
||||
->select('s.*')
|
||||
->from('tx_vitec_domain_model_solution', 's')
|
||||
->join('s', 'tx_vitec_product_solution_mm', 'mm', 'mm.uid_foreign = s.uid')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->eq('s.deleted', 0),
|
||||
$queryBuilder->expr()->eq('s.hidden', 0)
|
||||
)
|
||||
->orderBy('mm.sorting', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class);
|
||||
$result = [];
|
||||
foreach ($rows as $solutionRow) {
|
||||
$solutionUid = (int)$solutionRow['uid'];
|
||||
$result[] = [
|
||||
'uid' => $solutionUid,
|
||||
'title' => (string)($solutionRow['title'] ?? ''),
|
||||
'slug' => (string)($solutionRow['slug'] ?? ''),
|
||||
'subtitle' => (string)($solutionRow['subtitle'] ?? ''),
|
||||
'teaser' => (string)($solutionRow['teaser'] ?? ''),
|
||||
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
|
||||
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
|
||||
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getProductDownloads(int $productUid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
|
||||
@@ -252,6 +252,10 @@ class ProductShowJsonRenderer
|
||||
'ogimage' => $this->getProductOgImage($uid),
|
||||
'videofile' => $this->getProductVideoFile($uid),
|
||||
'relatedprodukt' => $this->getRelatedProducts($uid),
|
||||
'solutions' => $this->getProductSolutions($uid),
|
||||
// Detail-only like heroimage: resource cards are detail-page
|
||||
// furniture a list never shows.
|
||||
'resources' => $this->getProductResources($uid),
|
||||
];
|
||||
|
||||
// schema.org JSON-LD (Product + optional VideoObject) as a ready-to-emit
|
||||
@@ -732,6 +736,93 @@ class ProductShowJsonRenderer
|
||||
return $fallbackFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solutions linked to this product (tx_vitec_product_solution_mm, editor
|
||||
* order) in the shared solution card shape - the same fields the solution
|
||||
* list emits, so the front end reuses its card component. detailUrl is
|
||||
* null while a solution has no detail page (9.12 contract).
|
||||
*/
|
||||
protected function getProductSolutions(int $productUid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_vitec_domain_model_solution');
|
||||
$rows = $queryBuilder
|
||||
->select('s.*')
|
||||
->from('tx_vitec_domain_model_solution', 's')
|
||||
->join('s', 'tx_vitec_product_solution_mm', 'mm', 'mm.uid_foreign = s.uid')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->eq('s.deleted', 0),
|
||||
$queryBuilder->expr()->eq('s.hidden', 0)
|
||||
)
|
||||
->orderBy('mm.sorting', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class);
|
||||
$result = [];
|
||||
foreach ($rows as $solutionRow) {
|
||||
$solutionUid = (int)$solutionRow['uid'];
|
||||
$result[] = [
|
||||
'uid' => $solutionUid,
|
||||
'title' => (string)($solutionRow['title'] ?? ''),
|
||||
'slug' => (string)($solutionRow['slug'] ?? ''),
|
||||
'subtitle' => (string)($solutionRow['subtitle'] ?? ''),
|
||||
'teaser' => (string)($solutionRow['teaser'] ?? ''),
|
||||
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
|
||||
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
|
||||
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource cards from the product's Resources tab (child table
|
||||
* tx_vitec_product_resource): documentation / support / datasheet
|
||||
* entries, each an editable card (title, text, icon) around one link -
|
||||
* page, external URL, file or download record, resolved per 9.12. A card
|
||||
* whose link is empty or unresolvable is dropped (decision 2026-09-24).
|
||||
* List envelope: `layout` switches to `carousel` above three cards.
|
||||
*/
|
||||
protected function getProductResources(int $productUid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_vitec_product_resource');
|
||||
$rows = $queryBuilder
|
||||
->select('*')
|
||||
->from('tx_vitec_product_resource')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('product', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->eq('deleted', 0),
|
||||
$queryBuilder->expr()->eq('hidden', 0)
|
||||
)
|
||||
->orderBy('sorting', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$items = [];
|
||||
foreach ($rows as $resourceRow) {
|
||||
$url = \Evomedien\Vitec\Service\LinkResolver::typolinkUrl((string)($resourceRow['link'] ?? ''));
|
||||
if ($url === null) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'type' => (string)($resourceRow['restype'] ?? ''),
|
||||
'title' => (string)($resourceRow['title'] ?? ''),
|
||||
'text' => (string)($resourceRow['cardtext'] ?? ''),
|
||||
'icon' => (string)($resourceRow['icon'] ?? ''),
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'layout' => count($items) > 3 ? 'carousel' : 'grid',
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getProductDownloads(int $productUid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
|
||||
Reference in new issue
Block a user