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), an editable card title/text,
  an icon (free text with a value-picker seed set) and one link
  (page/url/file/record via the link wizard). Deliberately no Extbase
  model: only the UserFunc path reads the rows.
- datasheets: own MM selection of download records below the resource
  list (tx_vitec_product_datasheet_mm); emitted into the SAME
  resources envelope as ready-made type=datasheet cards (title/teaser/
  icon from the record, url = the public /download/<slug> route, uid
  fallback - private/hidden downloads 404 as everywhere)
- JSON `resources` (detail payload only, like heroimage): list envelope
  {layout, items[]}; a hand-written card whose link is empty or
  unresolvable is not emitted; layout switches to carousel above three
  cards in total
- solutions hybrid (same day, after analysing all 100 workbooks: their
  Solution Cards reference the 8 GROUPS in 98% of cases): new is_group
  flag on the solution model, new idempotent CLI
  vitec:create-solution-groups (8 group records; title source is
  AlignSolutionsCommand::TARGET - now public -, category = the group's
  own category, detail_page = the /solutions group page with -N
  disambiguation). Group records are invisible to the solution list
  and to vitec:align-solutions and only selectable in the product
  relation; product solution cards carry isGroup
- spec raised to v1.18 (7.15 member table, 7.13.3 note, 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:
orascheandClaude Fable 5 committed 2026-09-24 14:47:04 +02:00
1 parent 3df66b1353
commit 6811463343
12 files changed
+325 -12

No files matched your search

@@ -947,6 +947,7 @@ class ProductListJsonRenderer
'teaser' => (string)($solutionRow['teaser'] ?? ''),
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
'isGroup' => (bool)($solutionRow['is_group'] ?? false),
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
];
}
@@ -771,6 +771,7 @@ class ProductShowJsonRenderer
'teaser' => (string)($solutionRow['teaser'] ?? ''),
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
'isGroup' => (bool)($solutionRow['is_group'] ?? false),
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
];
}
@@ -817,6 +818,37 @@ class ProductShowJsonRenderer
];
}
// Datasheets are their own MM selection of download records and join
// the same envelope as ready-made cards: url is the public forced-
// download route (slug route since 2026-08-21, uid route as
// fallback), so the DownloadFileResolver rules apply unchanged -
// a private or hidden download simply 404s.
$datasheetQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_download');
$datasheetRows = $datasheetQueryBuilder
->select('d.uid', 'd.title', 'd.slug', 'd.teaser', 'd.icon')
->from('tx_vitec_domain_model_download', 'd')
->join('d', 'tx_vitec_product_datasheet_mm', 'mm', 'mm.uid_foreign = d.uid')
->where(
$datasheetQueryBuilder->expr()->eq('mm.uid_local', $datasheetQueryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$datasheetQueryBuilder->expr()->eq('d.deleted', 0),
$datasheetQueryBuilder->expr()->eq('d.hidden', 0)
)
->orderBy('mm.sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
foreach ($datasheetRows as $datasheetRow) {
$slug = trim((string)($datasheetRow['slug'] ?? ''), "/ ");
$items[] = [
'type' => 'datasheet',
'title' => (string)($datasheetRow['title'] ?? ''),
'text' => (string)($datasheetRow['teaser'] ?? ''),
'icon' => ((string)($datasheetRow['icon'] ?? '')) !== '' ? (string)$datasheetRow['icon'] : 'datasheet',
'url' => $slug !== '' ? '/download/' . $slug : '/download/file/' . (int)$datasheetRow['uid'],
];
}
return [
'layout' => count($items) > 3 ? 'carousel' : 'grid',
'items' => $items,
@@ -126,7 +126,10 @@ class SolutionListJsonRenderer
->from(self::TABLE)
->where(
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
$qb->expr()->eq('hidden', 0),
// Group-level records (hybrid 2026-09-24) exist for the
// product relation - never as a 40th+ list entry.
$qb->expr()->eq('is_group', 0)
)
->orderBy('title', 'ASC')
->executeQuery()