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:
1 parent
3df66b1353
commit
6811463343
12 files changed
+325
-12
No files matched your search
@@ -49,7 +49,7 @@ final class AlignSolutionsCommand extends Command
|
|||||||
private const TABLE = 'tx_vitec_domain_model_solution';
|
private const TABLE = 'tx_vitec_domain_model_solution';
|
||||||
|
|
||||||
/** Binding target: group => solutions, verbatim from the sheet (typo fixed). */
|
/** Binding target: group => solutions, verbatim from the sheet (typo fixed). */
|
||||||
private const TARGET = [
|
public const TARGET = [
|
||||||
'Control Room & Command Centre Solutions' => [
|
'Control Room & Command Centre Solutions' => [
|
||||||
'Control Room Platforms (SOC / NOC / Command Centres)',
|
'Control Room Platforms (SOC / NOC / Command Centres)',
|
||||||
'Command & Control Visualisation',
|
'Command & Control Visualisation',
|
||||||
@@ -170,8 +170,10 @@ final class AlignSolutionsCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
$solutions = [];
|
$solutions = [];
|
||||||
|
// is_group records (the 8 group-level cards, hybrid 2026-09-24) are
|
||||||
|
// invisible to this command: they are neither targets nor prunable.
|
||||||
foreach ($connection->fetchAllAssociative(
|
foreach ($connection->fetchAllAssociative(
|
||||||
'SELECT uid, title, slug FROM ' . self::TABLE . ' WHERE deleted = 0'
|
'SELECT uid, title, slug FROM ' . self::TABLE . ' WHERE deleted = 0 AND is_group = 0'
|
||||||
) as $row) {
|
) as $row) {
|
||||||
$solutions[(string)$row['title']] = ['uid' => (int)$row['uid'], 'slug' => (string)$row['slug']];
|
$solutions[(string)$row['title']] = ['uid' => (int)$row['uid'], 'slug' => (string)$row['slug']];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hybrid decision 2026-09-24: the eight solution GROUPS become selectable
|
||||||
|
* records (is_group = 1) so the product `solutions` relation can point at a
|
||||||
|
* group or at an individual Generic Solution - the agency workbooks
|
||||||
|
* reference the group level in 98% of their Solution Cards.
|
||||||
|
*
|
||||||
|
* Per group (source of truth: AlignSolutionsCommand::TARGET keys):
|
||||||
|
* - solution record, is_group = 1, slug from the title (collision-safe)
|
||||||
|
* - categories := the group's own sys_category (below root "Solution")
|
||||||
|
* - detail_page := the /solutions group page, matched by normalized title
|
||||||
|
* (rule of vitec:map-solution-pages; `-N` duplicate slugs lose,
|
||||||
|
* ambiguity is reported, never guessed)
|
||||||
|
*
|
||||||
|
* Group records are invisible to solution lists, to vitec:align-solutions
|
||||||
|
* and to Solr - they exist for the product relation. Idempotent: existing
|
||||||
|
* group records are left alone.
|
||||||
|
*
|
||||||
|
* vendor/bin/typo3 vitec:create-solution-groups --dry-run
|
||||||
|
* vendor/bin/typo3 vitec:create-solution-groups
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'vitec:create-solution-groups',
|
||||||
|
description: 'Create the 8 solution-group records (is_group=1) for the product solutions relation'
|
||||||
|
)]
|
||||||
|
final class CreateSolutionGroupsCommand extends Command
|
||||||
|
{
|
||||||
|
private const TABLE = 'tx_vitec_domain_model_solution';
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Report only - nothing written');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
Bootstrap::initializeBackendAuthentication();
|
||||||
|
$dryRun = (bool)$input->getOption('dry-run');
|
||||||
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE);
|
||||||
|
|
||||||
|
$groups = array_keys(AlignSolutionsCommand::TARGET);
|
||||||
|
|
||||||
|
// Existing records: titles for idempotency, slugs for collision checks.
|
||||||
|
$existingByTitle = [];
|
||||||
|
$usedSlugs = [];
|
||||||
|
foreach ($connection->fetchAllAssociative(
|
||||||
|
'SELECT uid, title, slug, is_group FROM ' . self::TABLE . ' WHERE deleted = 0'
|
||||||
|
) as $row) {
|
||||||
|
$existingByTitle[$this->normalize((string)$row['title'])] = $row;
|
||||||
|
$usedSlugs[(string)$row['slug']] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pids = $connection->fetchFirstColumn('SELECT pid FROM ' . self::TABLE . ' WHERE deleted = 0');
|
||||||
|
$pidCounts = array_count_values(array_map('intval', $pids));
|
||||||
|
arsort($pidCounts);
|
||||||
|
$storagePid = (int)(array_key_first($pidCounts) ?? 0);
|
||||||
|
if ($storagePid <= 0) {
|
||||||
|
$output->writeln('<error>No storage pid detectable (no solution records?)</error>');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group categories below the root category "Solution".
|
||||||
|
$rootCat = (int)($connection->fetchOne(
|
||||||
|
"SELECT uid FROM sys_category WHERE deleted = 0 AND parent = 0 AND title = 'Solution'"
|
||||||
|
) ?: 0);
|
||||||
|
$catByTitle = [];
|
||||||
|
if ($rootCat > 0) {
|
||||||
|
foreach ($connection->fetchAllAssociative(
|
||||||
|
'SELECT uid, title FROM sys_category WHERE deleted = 0 AND parent = ?',
|
||||||
|
[$rootCat]
|
||||||
|
) as $row) {
|
||||||
|
$catByTitle[$this->normalize((string)$row['title'])] = (int)$row['uid'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Candidate pages: standard pages, hidden allowed (map-solution-pages rule).
|
||||||
|
$pagesByTitle = [];
|
||||||
|
foreach ($connection->fetchAllAssociative(
|
||||||
|
'SELECT uid, title, slug FROM pages WHERE deleted = 0 AND doktype = 1'
|
||||||
|
) as $row) {
|
||||||
|
$pagesByTitle[$this->normalize((string)$row['title'])][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datamap = [];
|
||||||
|
$newIndex = 0;
|
||||||
|
$created = 0;
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$key = $this->normalize($group);
|
||||||
|
$existing = $existingByTitle[$key] ?? null;
|
||||||
|
if ($existing !== null) {
|
||||||
|
$state = ((int)$existing['is_group'] === 1) ? 'ok (is_group=1)' : '<error>KONFLIKT: Record gleichen Titels ohne is_group</error>';
|
||||||
|
$output->writeln(sprintf(' %-45s existiert, uid %d - %s', $group, (int)$existing['uid'], $state));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageUid = 0;
|
||||||
|
$candidates = $pagesByTitle[$key] ?? [];
|
||||||
|
if (count($candidates) > 1) {
|
||||||
|
// `-N` duplicate slugs lose the disambiguation.
|
||||||
|
$candidates = array_values(array_filter(
|
||||||
|
$candidates,
|
||||||
|
static fn(array $p): bool => !preg_match('/-\d+$/', (string)$p['slug'])
|
||||||
|
)) ?: $candidates;
|
||||||
|
}
|
||||||
|
if (count($candidates) === 1) {
|
||||||
|
$pageUid = (int)$candidates[0]['uid'];
|
||||||
|
} elseif (count($candidates) > 1) {
|
||||||
|
$output->writeln(sprintf(' <comment>%s: Seite mehrdeutig (%d Kandidaten) - detail_page bleibt leer</comment>', $group, count($candidates)));
|
||||||
|
} else {
|
||||||
|
$output->writeln(sprintf(' <comment>%s: keine Gruppen-Seite gefunden - detail_page bleibt leer</comment>', $group));
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $this->slugFor($group, $usedSlugs);
|
||||||
|
$fields = [
|
||||||
|
'pid' => $storagePid,
|
||||||
|
'title' => $group,
|
||||||
|
'slug' => $slug,
|
||||||
|
'is_group' => 1,
|
||||||
|
'detail_page' => $pageUid,
|
||||||
|
];
|
||||||
|
$catUid = $catByTitle[$key] ?? 0;
|
||||||
|
if ($catUid > 0) {
|
||||||
|
$fields['categories'] = (string)$catUid;
|
||||||
|
} else {
|
||||||
|
$output->writeln(sprintf(' <comment>%s: keine Gruppen-Kategorie gefunden</comment>', $group));
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
' %-45s %s slug=%s page=%s cat=%s',
|
||||||
|
$group,
|
||||||
|
$dryRun ? 'wuerde angelegt' : 'anlegen',
|
||||||
|
$slug,
|
||||||
|
$pageUid ?: '-',
|
||||||
|
$catUid ?: '-'
|
||||||
|
));
|
||||||
|
$datamap[self::TABLE]['NEWGROUP' . (++$newIndex)] = $fields;
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$output->writeln(sprintf('<info>DRY-RUN: %d Gruppen-Records wuerden angelegt.</info>', $created));
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($datamap !== []) {
|
||||||
|
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||||
|
$dataHandler->start($datamap, []);
|
||||||
|
$dataHandler->process_datamap();
|
||||||
|
foreach ($dataHandler->errorLog as $error) {
|
||||||
|
$output->writeln("<error>$error</error>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output->writeln(sprintf('<info>%d Gruppen-Records angelegt.</info>', $created));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalize(string $title): string
|
||||||
|
{
|
||||||
|
$title = str_replace('&', 'and', mb_strtolower($title));
|
||||||
|
$title = (string)preg_replace('/[^a-z0-9]+/', ' ', $title);
|
||||||
|
return trim((string)preg_replace('/\s+/', ' ', $title));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,bool> $usedSlugs */
|
||||||
|
private function slugFor(string $title, array &$usedSlugs): string
|
||||||
|
{
|
||||||
|
$slug = trim((string)preg_replace('/[^a-z0-9]+/', '-', str_replace('&', 'and', mb_strtolower($title))), '-');
|
||||||
|
$candidate = $slug;
|
||||||
|
$i = 1;
|
||||||
|
while (isset($usedSlugs[$candidate])) {
|
||||||
|
$candidate = $slug . '-group' . ($i > 1 ? '-' . $i : '');
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
$usedSlugs[$candidate] = true;
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -723,6 +723,7 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
|||||||
$this->heroimage = new ObjectStorage();
|
$this->heroimage = new ObjectStorage();
|
||||||
$this->downloads = new ObjectStorage();
|
$this->downloads = new ObjectStorage();
|
||||||
$this->solutions = new ObjectStorage();
|
$this->solutions = new ObjectStorage();
|
||||||
|
$this->datasheets = new ObjectStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1097,6 +1098,29 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
|
|||||||
$this->solutions = $solutions;
|
$this->solutions = $solutions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datasheet downloads for the Resources tab (MM)
|
||||||
|
*
|
||||||
|
* @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Download>
|
||||||
|
*/
|
||||||
|
protected $datasheets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Download>
|
||||||
|
*/
|
||||||
|
public function getDatasheets(): ObjectStorage
|
||||||
|
{
|
||||||
|
return $this->datasheets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> $datasheets
|
||||||
|
*/
|
||||||
|
public function setDatasheets(ObjectStorage $datasheets): void
|
||||||
|
{
|
||||||
|
$this->datasheets = $datasheets;
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -204,4 +204,22 @@ class Solution extends AbstractEntity
|
|||||||
{
|
{
|
||||||
$this->categories = $categories;
|
$this->categories = $categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group-level record (one of the 8 solution groups) - hybrid decision
|
||||||
|
* 2026-09-24. Excluded from lists; selectable in product relations.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $isGroup = false;
|
||||||
|
|
||||||
|
public function getIsGroup(): bool
|
||||||
|
{
|
||||||
|
return $this->isGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsGroup(bool $isGroup): void
|
||||||
|
{
|
||||||
|
$this->isGroup = $isGroup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -947,6 +947,7 @@ class ProductListJsonRenderer
|
|||||||
'teaser' => (string)($solutionRow['teaser'] ?? ''),
|
'teaser' => (string)($solutionRow['teaser'] ?? ''),
|
||||||
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
|
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
|
||||||
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
|
'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),
|
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -771,6 +771,7 @@ class ProductShowJsonRenderer
|
|||||||
'teaser' => (string)($solutionRow['teaser'] ?? ''),
|
'teaser' => (string)($solutionRow['teaser'] ?? ''),
|
||||||
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
|
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
|
||||||
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
|
'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),
|
'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 [
|
return [
|
||||||
'layout' => count($items) > 3 ? 'carousel' : 'grid',
|
'layout' => count($items) > 3 ? 'carousel' : 'grid',
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ class SolutionListJsonRenderer
|
|||||||
->from(self::TABLE)
|
->from(self::TABLE)
|
||||||
->where(
|
->where(
|
||||||
$qb->expr()->eq('deleted', 0),
|
$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')
|
->orderBy('title', 'ASC')
|
||||||
->executeQuery()
|
->executeQuery()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ return [
|
|||||||
],
|
],
|
||||||
'types' => [
|
'types' => [
|
||||||
'1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt, solutions,
|
'1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt, solutions,
|
||||||
--div--;Resources, resources,
|
--div--;Resources, resources, datasheets,
|
||||||
--div--;SEO, seotitle, urltitle, seometa, keywords, structureddata,
|
--div--;SEO, seotitle, urltitle, seometa, keywords, structureddata,
|
||||||
--div--;Images and Videos, heroimage, productimage, ogimage, video, videofile,
|
--div--;Images and Videos, heroimage, productimage, ogimage, video, videofile,
|
||||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
|
||||||
@@ -463,6 +463,21 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'datasheets' => [
|
||||||
|
'exclude' => true,
|
||||||
|
'label' => 'Datasheets',
|
||||||
|
'description' => 'Download records shown as datasheet cards below the resource cards - same card row, counts towards the carousel rule',
|
||||||
|
'config' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'renderType' => 'selectMultipleSideBySide',
|
||||||
|
'foreign_table' => 'tx_vitec_domain_model_download',
|
||||||
|
'MM' => 'tx_vitec_product_datasheet_mm',
|
||||||
|
'size' => 8,
|
||||||
|
'maxitems' => 9999,
|
||||||
|
'minitems' => 0,
|
||||||
|
'enableMultiSelectFilterTextfield' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
'productimage' => [
|
'productimage' => [
|
||||||
'exclude' => true,
|
'exclude' => true,
|
||||||
'label' => 'Product Image(s)',
|
'label' => 'Product Image(s)',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ return [
|
|||||||
],
|
],
|
||||||
'types' => [
|
'types' => [
|
||||||
'1' => [
|
'1' => [
|
||||||
'showitem' => 'title, slug, detail_page, subtitle, teaser, description, image,
|
'showitem' => 'title, slug, detail_page, is_group, subtitle, teaser, description, image,
|
||||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
|
||||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent, l10n_diffsource,
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent, l10n_diffsource,
|
||||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime',
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime',
|
||||||
@@ -140,6 +140,16 @@ return [
|
|||||||
'maxitems' => 1,
|
'maxitems' => 1,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'is_group' => [
|
||||||
|
'exclude' => true,
|
||||||
|
'label' => 'Is Group',
|
||||||
|
'description' => 'Group-level record (one of the 8 solution groups, hybrid decision 2026-09-24). Kept out of solution lists and vitec:align-solutions; selectable in the product "Related Solutions" field.',
|
||||||
|
'config' => [
|
||||||
|
'type' => 'check',
|
||||||
|
'renderType' => 'checkboxToggle',
|
||||||
|
'default' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
'subtitle' => [
|
'subtitle' => [
|
||||||
'exclude' => true,
|
'exclude' => true,
|
||||||
'label' => 'Subtitle',
|
'label' => 'Subtitle',
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resource cards on the product's Resources tab (decision 2026-09-24):
|
* Resource cards on the product's Resources tab (decision 2026-09-24):
|
||||||
* documentation / support / datasheet entries, several per type, each an
|
* documentation / support entries, several per type, each an editable card
|
||||||
* editable card (title, text, icon) around one link. Child table of
|
* (title, text, icon) around one link. Datasheets are NOT an option here -
|
||||||
|
* they are the `datasheets` MM selection of download records next to this
|
||||||
|
* list and join the same JSON envelope as ready-made cards. Child table of
|
||||||
* tx_vitec_domain_model_product via the `resources` inline field - edited
|
* tx_vitec_domain_model_product via the `resources` inline field - edited
|
||||||
* only there (hideTable), no Extbase model on purpose: the headless
|
* only there (hideTable), no Extbase model on purpose: the headless
|
||||||
* renderers query the rows directly, the Extbase path never touches them.
|
* renderers query the rows directly, the Extbase path never touches them.
|
||||||
@@ -59,7 +61,6 @@ return [
|
|||||||
'items' => [
|
'items' => [
|
||||||
['label' => 'Product Documentation', 'value' => 'documentation'],
|
['label' => 'Product Documentation', 'value' => 'documentation'],
|
||||||
['label' => 'Technical Support', 'value' => 'support'],
|
['label' => 'Technical Support', 'value' => 'support'],
|
||||||
['label' => 'Datasheet', 'value' => 'datasheet'],
|
|
||||||
],
|
],
|
||||||
'default' => 'documentation',
|
'default' => 'documentation',
|
||||||
],
|
],
|
||||||
@@ -97,7 +98,6 @@ return [
|
|||||||
'items' => [
|
'items' => [
|
||||||
['label' => 'Document', 'value' => 'document'],
|
['label' => 'Document', 'value' => 'document'],
|
||||||
['label' => 'Support', 'value' => 'support'],
|
['label' => 'Support', 'value' => 'support'],
|
||||||
['label' => 'Datasheet', 'value' => 'datasheet'],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
| 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. |
|
| 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. |
|
||||||
| 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. |
|
| 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. |
|
||||||
| 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. |
|
| 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. |
|
||||||
| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support / datasheet cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted), delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards (7.15). Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). |
|
| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted) plus a `datasheets` MM selection of download records rendered into the same envelope as `type: datasheet` cards, delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards in total (7.15). The `solutions` relation accepts group‑level records: the eight groups exist as `is_group = 1` solutions (created by `vitec:create-solution-groups`, excluded from lists and from `vitec:align-solutions`) because the agency workbooks reference the group level. Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). |
|
||||||
|
|
||||||
This document is drafted in the style of, and adopts the terminology conventions of,
|
This document is drafted in the style of, and adopts the terminology conventions of,
|
||||||
ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for
|
ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for
|
||||||
@@ -801,6 +801,12 @@ practice. Image resolution is delegated to `UsecaseSerializer::image()` per 9.2;
|
|||||||
renderer duplicates no FAL logic.
|
renderer duplicates no FAL logic.
|
||||||
|
|
||||||
#### 7.13.3 Solution list payload (`vitec_solutionlist`)
|
#### 7.13.3 Solution list payload (`vitec_solutionlist`)
|
||||||
|
|
||||||
|
> **NOTE (since v1.18)** Group‑level solution records (`is_group = 1`, the eight
|
||||||
|
> thematic groups) never appear in this list. They exist to be selected in the
|
||||||
|
> product `solutions` relation (7.15) and carry the group page as `detailUrl`;
|
||||||
|
> `vitec:align-solutions` ignores them, `vitec:create-solution-groups` creates
|
||||||
|
> them idempotently.
|
||||||
Added in v1.9, and deliberately the same shape as 7.13.2 — `SolutionListJsonRenderer` emits
|
Added in v1.9, and deliberately the same shape as 7.13.2 — `SolutionListJsonRenderer` emits
|
||||||
under `content.solutions`:
|
under `content.solutions`:
|
||||||
|
|
||||||
@@ -898,8 +904,8 @@ Produced by `ProductShowJsonRenderer::serializeProduct()`. Members:
|
|||||||
| `downloads` | array | download records incl. the resolved `file` |
|
| `downloads` | array | download records incl. the resolved `file` |
|
||||||
| `ogimage`, `videofile` | object \| null | FAL singletons |
|
| `ogimage`, `videofile` | object \| null | FAL singletons |
|
||||||
| `relatedprodukt` | array | card shape `{uid, title, slug, subtitle, teaser, description, link, images}`, backend order |
|
| `relatedprodukt` | array | card shape `{uid, title, slug, subtitle, teaser, description, link, images}`, backend order |
|
||||||
| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, image}` via `tx_vitec_product_solution_mm`, backend order |
|
| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, isGroup, image}` via `tx_vitec_product_solution_mm`, backend order; an editor may select individual Generic Solutions or the eight group‑level records (`isGroup: true`, linking to the group page) |
|
||||||
| `resources` | object | list envelope `{layout, items[]}` of hand‑written resource cards: `type` (`documentation\|support\|datasheet`), `title`, `text`, `icon`, resolved `url` (9.12); a card without a resolvable link is dropped; `layout` is `carousel` above three cards — **detail payload only** |
|
| `resources` | object | list envelope `{layout, items[]}`: hand‑written cards (`type` `documentation\|support`, `title`, `text`, `icon`, resolved `url` per 9.12; a card without a resolvable link is dropped) followed by the `datasheets` MM selection of download records as `type: datasheet` cards (title/teaser/icon from the record, `url` = the public download route); `layout` is `carousel` above three cards in total — **detail payload only** |
|
||||||
| `jsonLd` | string | ready‑to‑emit schema.org `@graph` (6.6), detail only |
|
| `jsonLd` | string | ready‑to‑emit schema.org `@graph` (6.6), detail only |
|
||||||
|
|
||||||
#### 7.15.2 List payload (`vitec_productlist`, key `products`)
|
#### 7.15.2 List payload (`vitec_productlist`, key `products`)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ CREATE TABLE tx_vitec_domain_model_product (
|
|||||||
textrelatedproducts text,
|
textrelatedproducts text,
|
||||||
solutions int(11) DEFAULT '0' NOT NULL,
|
solutions int(11) DEFAULT '0' NOT NULL,
|
||||||
resources int(11) DEFAULT '0' NOT NULL,
|
resources int(11) DEFAULT '0' NOT NULL,
|
||||||
|
datasheets int(11) DEFAULT '0' NOT NULL,
|
||||||
KEY parent (pid)
|
KEY parent (pid)
|
||||||
);
|
);
|
||||||
CREATE TABLE tx_vitec_domain_model_download (
|
CREATE TABLE tx_vitec_domain_model_download (
|
||||||
@@ -106,6 +107,13 @@ CREATE TABLE tx_vitec_product_solution_mm (
|
|||||||
PRIMARY KEY (uid_local, uid_foreign)
|
PRIMARY KEY (uid_local, uid_foreign)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tx_vitec_product_datasheet_mm (
|
||||||
|
uid_local INT(11) NOT NULL,
|
||||||
|
uid_foreign INT(11) NOT NULL,
|
||||||
|
sorting INT(11) DEFAULT '0' NOT NULL,
|
||||||
|
PRIMARY KEY (uid_local, uid_foreign)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE tx_vitec_product_resource (
|
CREATE TABLE tx_vitec_product_resource (
|
||||||
uid int(11) NOT NULL auto_increment,
|
uid int(11) NOT NULL auto_increment,
|
||||||
pid int(11) DEFAULT '0' NOT NULL,
|
pid int(11) DEFAULT '0' NOT NULL,
|
||||||
@@ -190,6 +198,7 @@ CREATE TABLE tx_vitec_domain_model_solution (
|
|||||||
title varchar(255) DEFAULT '' NOT NULL,
|
title varchar(255) DEFAULT '' NOT NULL,
|
||||||
slug varchar(255) DEFAULT '' NOT NULL,
|
slug varchar(255) DEFAULT '' NOT NULL,
|
||||||
detail_page int(11) DEFAULT '0' NOT NULL,
|
detail_page int(11) DEFAULT '0' NOT NULL,
|
||||||
|
is_group tinyint(4) DEFAULT '0' NOT NULL,
|
||||||
subtitle varchar(255) DEFAULT '' NOT NULL,
|
subtitle varchar(255) DEFAULT '' NOT NULL,
|
||||||
teaser varchar(255) DEFAULT '' NOT NULL,
|
teaser varchar(255) DEFAULT '' NOT NULL,
|
||||||
description text,
|
description text,
|
||||||
|
|||||||
Reference in new issue
Block a user