Product text import from agency XLSX workbooks
VITEC Import module: Products tab is now a searchable/sortable product overview. "Edit Product" opens an XLSX upload with per-field source mapping (component + cell), old/new preview and checkbox apply via DataHandler; the mapping and manual matches are persisted and preselect the next workbook. Category workbooks are detected and rejected. - new: ProductXlsxReader (PhpSpreadsheet), ProductTextImportController, Products/ProductTexts templates, 4 module routes - related products: per-pair card text in new side table tx_vitec_product_related_text (survives MM rewrites), emitted as `cardtext` in the product JSON; missing MM relations added add-only - card copy read from Body Copy (D) with fallback to CTA/Card Copy (E) - the workbooks fill either depending on row type - product detail page <title> now uses seotitle with title fallback (provider made singleton and fed from the JSON renderer) - per-user recent-search badges; last loaded workbook stored per product (tx_vitec_product_workbook), Edit Product reopens on it - composer: add phpoffice/phpspreadsheet ^5.9 - diagnostics: migrations/check_workbook.php
This commit is contained in:
1 parent
12eb40614b
commit
7bd1161abf
28 files changed
+2594
-39
No files matched your search
@@ -205,11 +205,15 @@ class ProductListJsonRenderer
|
||||
'subtitle' => (string)($product['subtitle'] ?? ''),
|
||||
'video' => (string)($product['video'] ?? ''),
|
||||
'applications' => RteResolver::html($product['applications'] ?? ''),
|
||||
'capabilities' => RteResolver::html($product['capabilities'] ?? ''),
|
||||
'description' => RteResolver::html($product['description'] ?? ''),
|
||||
'description2' => RteResolver::html($product['description2'] ?? ''),
|
||||
'highlights' => RteResolver::html($product['highlights'] ?? ''),
|
||||
'textrelatedproducts' => RteResolver::html($product['textrelatedproducts'] ?? ''),
|
||||
'shortcutpid' => (string)($product['shortcutpid'] ?? ''),
|
||||
'contentelement' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelement'] ?? '')),
|
||||
'contentelementcta' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelementcta'] ?? '')),
|
||||
'portfolio' => \Evomedien\Vitec\Service\LinkResolver::typolinkUrl((string)($product['portfolio'] ?? '')),
|
||||
|
||||
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
||||
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
||||
|
||||
@@ -164,6 +164,19 @@ class ProductShowJsonRenderer
|
||||
: '';
|
||||
}
|
||||
|
||||
// Page <title> for the detail page: the product's seotitle, falling
|
||||
// back to its title. Fed from here because in headless mode the
|
||||
// Extbase showAction (which used to do this) never runs; headless's
|
||||
// MetaHandler reads the provider chain when composing `seo`.
|
||||
$pageTitle = trim((string)($product['seotitle'] ?? ''));
|
||||
if ($pageTitle === '') {
|
||||
$pageTitle = trim((string)($product['title'] ?? ''));
|
||||
}
|
||||
if ($pageTitle !== '') {
|
||||
GeneralUtility::makeInstance(\Evomedien\Vitec\PageTitle\ProductPageTitleProvider::class)
|
||||
->setSeoTitle($pageTitle);
|
||||
}
|
||||
|
||||
$response = [
|
||||
'product' => $this->serializeProduct($product),
|
||||
'layout' => $layout,
|
||||
@@ -210,11 +223,15 @@ class ProductShowJsonRenderer
|
||||
'subtitle' => (string)($product['subtitle'] ?? ''),
|
||||
'video' => (string)($product['video'] ?? ''),
|
||||
'applications' => RteResolver::html($product['applications'] ?? ''),
|
||||
'capabilities' => RteResolver::html($product['capabilities'] ?? ''),
|
||||
'description' => RteResolver::html($product['description'] ?? ''),
|
||||
'description2' => RteResolver::html($product['description2'] ?? ''),
|
||||
'highlights' => RteResolver::html($product['highlights'] ?? ''),
|
||||
'textrelatedproducts' => RteResolver::html($product['textrelatedproducts'] ?? ''),
|
||||
'shortcutpid' => (string)($product['shortcutpid'] ?? ''),
|
||||
'contentelement' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelement'] ?? '')),
|
||||
'contentelementcta' => \Evomedien\Vitec\Service\ContentElementResolver::resolveLink((string)($product['contentelementcta'] ?? '')),
|
||||
'portfolio' => \Evomedien\Vitec\Service\LinkResolver::typolinkUrl((string)($product['portfolio'] ?? '')),
|
||||
|
||||
'hideonapp' => (bool)($product['hideonapp'] ?? false),
|
||||
'hideonwebsite' => (bool)($product['hideonwebsite'] ?? false),
|
||||
@@ -230,6 +247,7 @@ class ProductShowJsonRenderer
|
||||
|
||||
'categories' => $this->getProductCategories($uid),
|
||||
'images' => $this->getProductImages($uid),
|
||||
'heroimage' => $this->getProductImages($uid, 'heroimage'),
|
||||
'downloads' => $this->getProductDownloads($uid),
|
||||
'ogimage' => $this->getProductOgImage($uid),
|
||||
'videofile' => $this->getProductVideoFile($uid),
|
||||
@@ -310,7 +328,7 @@ class ProductShowJsonRenderer
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function getProductImages(int $productUid): array
|
||||
protected function getProductImages(int $productUid, string $fieldName = 'productimage'): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_file_reference');
|
||||
@@ -321,7 +339,7 @@ class ProductShowJsonRenderer
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)),
|
||||
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('productimage', ParameterType::STRING)),
|
||||
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)),
|
||||
$queryBuilder->expr()->eq('deleted', 0),
|
||||
$queryBuilder->expr()->eq('hidden', 0)
|
||||
)
|
||||
@@ -805,6 +823,7 @@ class ProductShowJsonRenderer
|
||||
|
||||
$related = $queryBuilder
|
||||
->select('p.uid', 'p.title', 'p.slug', 'p.subtitle', 'p.teaser', 'p.description')
|
||||
->addSelectLiteral('t.cardtext')
|
||||
->from('tx_vitec_domain_model_product', 'p')
|
||||
->join(
|
||||
'p',
|
||||
@@ -812,6 +831,16 @@ class ProductShowJsonRenderer
|
||||
'mm',
|
||||
'mm.uid_foreign = p.uid'
|
||||
)
|
||||
// Per-pair card copy from the workbook import. A separate table
|
||||
// rather than an MM column, because DataHandler rewrites the MM
|
||||
// rows on every product save - see tx_vitec_product_related_text
|
||||
// in ext_tables.sql.
|
||||
->leftJoin(
|
||||
'mm',
|
||||
'tx_vitec_product_related_text',
|
||||
't',
|
||||
't.product_uid = mm.uid_local AND t.related_uid = p.uid'
|
||||
)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->eq('p.deleted', 0),
|
||||
@@ -831,6 +860,7 @@ class ProductShowJsonRenderer
|
||||
'subtitle' => (string)($rel['subtitle'] ?? ''),
|
||||
'teaser' => (string)($rel['teaser'] ?? ''),
|
||||
'description' => RteResolver::html($rel['description'] ?? ''),
|
||||
'cardtext' => (string)($rel['cardtext'] ?? ''),
|
||||
'link' => '/product/' . (string)($rel['slug'] ?? ''),
|
||||
'images' => $this->getProductImages($relUid),
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user