Success stories: downloads and product sections become real plugins
- comparison report migrations/check_product_downloads.php (kept, read-only): download records vs. the delivered product workbooks - 34 of 74 workbook products have a datasheet, the entire Datapath line has none in the pool (import came from vitec.com; queued for Joe), ~35 orphan datasheets belong to products without records; a vitec:link-product-downloads re-run added seven clean links (Artio x2, Diamond-H, the three TOUGH records, X-View) - RTE resolution fixed at the root: ContentElementResolver and ContainerChildrenProcessor passed bodytext RAW into the payload, so t3:// links leaked unresolved; both now run RTE columns (bodytext, vitec_quote) through RteResolver::html() - the imported "Download Success Story" RTE buttons carried the wrong link identifier (tx_vitec_download -> download, one-time migration fix_download_record_links.php) AND old-site uids - missing here or, worse, accidentally pointing at foreign PDFs. New vitec:convert-story-download-buttons replaces them with real vitec_downloadcard plugins: the download is resolved by STORY title (manual map for ISR / "Okada Manilla", exact-title fallback for suffixless PDFs like "Lord's Cricket Ground"); 29 converted, 4 stories without a delivered PDF have their button hidden. New FlexForm layout "Success Story Download" (story_download) applied everywhere; the FE renders it as the previous centered button - the ADIB "Featured Products" text section (the only one site-wide, verified by scan_story_headers.php) is now ONE vitec_productlist with hand-picked products via old-name aliases and the new FlexForm layout "Success Story Products" (story_products); the four text elements are deleted. vitec:convert-story-product-sections stays re-runnable for future imports - the productlist envelope additionally carries "columns" (= number of products, capped at 4) so the story grid renders as many columns as it has products - additive key, available to every product list; and hand-picked lists now ship successStories as an empty array (the empty category selection used to aggregate stories over the WHOLE tree, leaking story cards into the section) - the story downloadcards carry header "Downloads" (set on convert and backfilled): the front end builds the story anchor nav from element headers, so the section now appears there - diagnosis scripts kept: check_download_record, check_story_elements, scan_story_headers; verified live on the ADIB payload (structured download object, productlist [5,18,6], texts 10 -> 5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
80f6da7949
commit
d87e64f73b
12 files changed
+762
-1
No files matched your search
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Diagnose: state of one download record (uid as argv). READ-ONLY.
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$uid = (int)($argv[1] ?? 0);
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
$r = $pdo->query("SELECT uid, pid, title, slug, hidden, deleted, starttime, endtime, private_download, file, fileprefix, useolddl FROM tx_vitec_domain_model_download WHERE uid = $uid")->fetch();
|
||||
print_r($r ?: "not found\n");
|
||||
if ($r) {
|
||||
foreach ($pdo->query("SELECT r.uid, r.uid_local, f.identifier FROM sys_file_reference r LEFT JOIN sys_file f ON f.uid = r.uid_local WHERE r.deleted = 0 AND r.tablenames = 'tx_vitec_domain_model_download' AND r.uid_foreign = $uid") as $ref) { print_r($ref); }
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Comparison report: download records vs. the delivered product workbooks
|
||||
* (request Olli 2026-10-28). Per product: linked downloads
|
||||
* (tx_vitec_product_download_mm, uid_local = product), datasheet coverage
|
||||
* (download title suffix "datasheet" or a Datasheet category), the manual
|
||||
* Resources-tab selection (tx_vitec_product_datasheet_mm) - matched against
|
||||
* the workbook coverage from migrations/products_xlsx/manifest.json.
|
||||
*
|
||||
* READ-ONLY. Usage: php migrations/check_product_downloads.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
// --- workbook coverage from the manifest ------------------------------------
|
||||
$manifest = json_decode((string)file_get_contents($root . '/migrations/products_xlsx/manifest.json'), true) ?: [];
|
||||
$workbookSlugs = [];
|
||||
foreach (($manifest['workbooks'] ?? []) as $file => $entry) {
|
||||
if (!empty($entry['product'])) {
|
||||
$workbookSlugs[(string)$entry['product']] = basename((string)$file);
|
||||
}
|
||||
}
|
||||
|
||||
// --- data ---------------------------------------------------------------------
|
||||
$products = $pdo->query('SELECT uid, title, slug, hidden, legacy, supportproduct, subproduct FROM tx_vitec_domain_model_product WHERE deleted = 0 ORDER BY title')->fetchAll();
|
||||
$downloads = $pdo->query('SELECT uid, title, hidden FROM tx_vitec_domain_model_download WHERE deleted = 0')->fetchAll();
|
||||
$downloadTitle = array_column($downloads, 'title', 'uid');
|
||||
|
||||
$linksByProduct = [];
|
||||
$linkedDownloadUids = [];
|
||||
foreach ($pdo->query('SELECT uid_local, uid_foreign FROM tx_vitec_product_download_mm') as $r) {
|
||||
$linksByProduct[(int)$r['uid_local']][] = (int)$r['uid_foreign'];
|
||||
$linkedDownloadUids[(int)$r['uid_foreign']] = true;
|
||||
}
|
||||
$sheetsByProduct = [];
|
||||
foreach ($pdo->query('SELECT uid_local, COUNT(*) c FROM tx_vitec_product_datasheet_mm GROUP BY uid_local') as $r) {
|
||||
$sheetsByProduct[(int)$r['uid_local']] = (int)$r['c'];
|
||||
}
|
||||
|
||||
$isDatasheet = static function (string $title): bool {
|
||||
return (bool)preg_match('/data\s*sheet\s*$/i', trim($title));
|
||||
};
|
||||
$docType = static function (string $title): string {
|
||||
foreach (['datasheet', 'data sheet', 'brochure', 'user guide', 'quick start guide', 'application note',
|
||||
'white paper', 'case study', 'success story', 'manual', 'firmware', 'software', 'flyer', 'guide'] as $suffix) {
|
||||
if (preg_match('/' . preg_quote($suffix, '/') . '\s*$/i', trim($title))) {
|
||||
return $suffix;
|
||||
}
|
||||
}
|
||||
return '(no type suffix)';
|
||||
};
|
||||
|
||||
// --- per-product evaluation ------------------------------------------------------
|
||||
$noDownloads = [];
|
||||
$noDatasheet = [];
|
||||
$okCount = 0;
|
||||
$withoutWorkbook = [];
|
||||
foreach ($products as $p) {
|
||||
$uid = (int)$p['uid'];
|
||||
$slug = (string)$p['slug'];
|
||||
$linked = $linksByProduct[$uid] ?? [];
|
||||
$hasSheet = false;
|
||||
foreach ($linked as $dUid) {
|
||||
if ($isDatasheet((string)($downloadTitle[$dUid] ?? ''))) { $hasSheet = true; break; }
|
||||
}
|
||||
$flag = ((int)$p['legacy'] ? ' [legacy]' : '') . ((int)$p['hidden'] ? ' [hidden]' : '')
|
||||
. ((int)$p['supportproduct'] ? ' [support]' : '') . ((int)$p['subproduct'] ? ' [sub]' : '');
|
||||
$line = sprintf('#%-4d %-45s dl=%-2d sheetsTab=%d%s', $uid, $p['title'], count($linked), $sheetsByProduct[$uid] ?? 0, $flag);
|
||||
if (!isset($workbookSlugs[$slug])) {
|
||||
$withoutWorkbook[] = $line;
|
||||
}
|
||||
if (isset($workbookSlugs[$slug])) {
|
||||
if ($linked === []) { $noDownloads[] = $line; }
|
||||
elseif (!$hasSheet) { $noDatasheet[] = $line; }
|
||||
else { $okCount++; }
|
||||
}
|
||||
}
|
||||
|
||||
// --- orphan downloads ---------------------------------------------------------------
|
||||
$orphans = [];
|
||||
$orphanTypes = [];
|
||||
foreach ($downloads as $d) {
|
||||
if (!isset($linkedDownloadUids[(int)$d['uid']])) {
|
||||
$type = $docType((string)$d['title']);
|
||||
$orphanTypes[$type] = ($orphanTypes[$type] ?? 0) + 1;
|
||||
$orphans[] = $d['title'];
|
||||
}
|
||||
}
|
||||
|
||||
printf("Produkte: %d (davon %d mit Workbook) · Downloads: %d (davon %d verlinkt, %d verwaist)\n\n",
|
||||
count($products), count($workbookSlugs), count($downloads), count($linkedDownloadUids), count($orphans));
|
||||
|
||||
echo "=== A) Workbook-Produkte OHNE verknuepfte Downloads (" . count($noDownloads) . ") ===\n";
|
||||
foreach ($noDownloads as $l) { echo " $l\n"; }
|
||||
|
||||
echo "\n=== B) Workbook-Produkte mit Downloads, aber OHNE Datasheet (" . count($noDatasheet) . ") ===\n";
|
||||
foreach ($noDatasheet as $l) { echo " $l\n"; }
|
||||
|
||||
echo "\n=== C) Workbook-Produkte mit Datasheet-Abdeckung: $okCount ===\n";
|
||||
|
||||
echo "\n=== D) Produkte OHNE Workbook (" . count($withoutWorkbook) . ") - zur Einordnung ===\n";
|
||||
foreach ($withoutWorkbook as $l) { echo " $l\n"; }
|
||||
|
||||
arsort($orphanTypes);
|
||||
echo "\n=== E) Verwaiste Downloads nach Typ ===\n";
|
||||
foreach ($orphanTypes as $type => $n) { printf(" %-22s %d\n", $type, $n); }
|
||||
echo "\n Beispiele (max 30):\n";
|
||||
foreach (array_slice($orphans, 0, 30) as $t) { echo " - $t\n"; }
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Diagnose: ordered content elements of one usecase (argv: usecase uid,
|
||||
// default 12 = ADIB) incl. columns children. READ-ONLY.
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$ucUid = (int)($argv[1] ?? 12);
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
$story = $pdo->query("SELECT uid, title, products FROM tx_vitec_domain_model_usecase WHERE uid = $ucUid")->fetch();
|
||||
echo "Story #{$story['uid']} \"{$story['title']}\" products-MM-count-field={$story['products']}\n";
|
||||
echo "verknuepfte Produkte: ";
|
||||
foreach ($pdo->query("SELECT p.uid, p.title FROM tx_vitec_usecase_product_mm mm JOIN tx_vitec_domain_model_product p ON p.uid = mm.uid_foreign WHERE mm.uid_local = $ucUid ORDER BY mm.sorting") as $p) {
|
||||
echo "#{$p['uid']} {$p['title']} | ";
|
||||
}
|
||||
echo "\n\n";
|
||||
function dump(PDO $pdo, string $where, int $depth): void {
|
||||
foreach ($pdo->query("SELECT uid, CType, LEFT(header,45) h, colPos, sorting, hidden, LEFT(REPLACE(REPLACE(COALESCE(bodytext,''),CHAR(10),' '),CHAR(13),''),70) b FROM tt_content WHERE deleted = 0 AND $where ORDER BY sorting") as $r) {
|
||||
printf("%s#%-5d %-20s col=%-4d sort=%-6d hid=%d %-45s | %s\n", str_repeat(' ', $depth), $r['uid'], $r['CType'], $r['colPos'], $r['sorting'], $r['hidden'], $r['h'], $r['b']);
|
||||
dump($pdo, "tx_container_parent = {$r['uid']}", $depth + 1);
|
||||
}
|
||||
}
|
||||
dump($pdo, "tx_vitec_usecase_content = $ucUid AND tx_container_parent = 0", 0);
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Repairs the record links the success-story import wrote into RTE bodies:
|
||||
* it used t3://record?identifier=tx_vitec_download&uid=N (the TABLE name),
|
||||
* but the configured link handler key - and therefore the only identifier
|
||||
* config.recordLinks resolves - is `download`. Result: raw t3://record
|
||||
* hrefs leaked into the JSON payload ("Download Success Story" buttons,
|
||||
* found 2026-10-28).
|
||||
*
|
||||
* This migration rewrites identifier=tx_vitec_download -> identifier=download
|
||||
* in tt_content.bodytext (plain and &-encoded variants). One-time,
|
||||
* idempotent, dry-run by default.
|
||||
*
|
||||
* Usage: php migrations/fix_download_record_links.php (dry run)
|
||||
* php migrations/fix_download_record_links.php --apply
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) { exit("no project root\n"); }
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '', $db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$needle = 'identifier=tx_vitec_download';
|
||||
$rows = $pdo->query(
|
||||
"SELECT uid, pid, CType, LEFT(header, 40) h FROM tt_content
|
||||
WHERE deleted = 0 AND bodytext LIKE '%identifier=tx_vitec_download%' ORDER BY pid, uid"
|
||||
)->fetchAll();
|
||||
|
||||
echo ($apply ? "APPLY\n" : "DRY-RUN\n");
|
||||
echo count($rows) . " element(s) carry the legacy identifier:\n";
|
||||
foreach ($rows as $r) {
|
||||
printf(" #%-6d pid=%-5d %-18s %s\n", $r['uid'], $r['pid'], $r['CType'], $r['h']);
|
||||
}
|
||||
if (!$apply) {
|
||||
exit("DRY RUN - add --apply to rewrite identifier=tx_vitec_download -> identifier=download.\n");
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE tt_content SET bodytext = REPLACE(bodytext, 'identifier=tx_vitec_download', 'identifier=download'),
|
||||
tstamp = UNIX_TIMESTAMP() WHERE uid = ?"
|
||||
);
|
||||
foreach ($rows as $r) {
|
||||
$stmt->execute([(int)$r['uid']]);
|
||||
}
|
||||
echo count($rows) . " element(s) rewritten. Flush the cache, then re-check a story payload.\n";
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Scan: all text-element headers inside usecase content, grouped. READ-ONLY.
|
||||
error_reporting(E_ALL); ini_set('display_errors', '1');
|
||||
$root = null; $dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) { if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; } $dir = dirname($dir); }
|
||||
if ($root === null) { exit("no root\n"); }
|
||||
$db = (require $root . '/config/system/settings.php')['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), $db['user'] ?? '', $db['password'] ?? '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
|
||||
echo "=== Header von text-Elementen in Story-Content (gruppiert) ===\n";
|
||||
foreach ($pdo->query("SELECT TRIM(header) h, COUNT(*) c FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content > 0 AND CType = 'text' AND TRIM(COALESCE(header,'')) <> '' GROUP BY TRIM(header) ORDER BY c DESC, h") as $r) {
|
||||
printf(" %3d x %s\n", $r['c'], $r['h']);
|
||||
}
|
||||
echo "\n=== CTypes in Story-Content gesamt ===\n";
|
||||
foreach ($pdo->query("SELECT CType, COUNT(*) c FROM tt_content WHERE deleted = 0 AND tx_vitec_usecase_content > 0 GROUP BY CType ORDER BY c DESC") as $r) {
|
||||
printf(" %3d x %s\n", $r['c'], $r['CType']);
|
||||
}
|
||||
|
||||
echo "\n=== Header von Kind-Elementen in Story-vitec_columns ===\n";
|
||||
foreach ($pdo->query(
|
||||
"SELECT TRIM(c.header) h, c.CType, COUNT(*) n FROM tt_content c
|
||||
WHERE c.deleted = 0 AND TRIM(COALESCE(c.header,'')) <> ''
|
||||
AND c.tx_container_parent IN (
|
||||
SELECT p.uid FROM tt_content p
|
||||
WHERE p.deleted = 0 AND p.tx_vitec_usecase_content > 0 AND p.CType = 'vitec_columns')
|
||||
GROUP BY TRIM(c.header), c.CType ORDER BY n DESC, h"
|
||||
) as $r) {
|
||||
printf(" %3d x %-14s %s\n", $r['n'], $r['CType'], $r['h']);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Converts the success-story "Download Success Story" RTE buttons into real
|
||||
* vitec_downloadcard plugins (decision Olli 2026-10-28).
|
||||
*
|
||||
* Background: the story import wrote text elements whose RTE body carries
|
||||
* `t3://record?identifier=...&uid=N` links built from the OLD site's
|
||||
* download uids - #259 etc. do not exist here, an accidentally existing
|
||||
* number may point at a FOREIGN pdf, and the front end would have to parse
|
||||
* HTML to render a button. The Downloadcard plugin solves all of it:
|
||||
* `settings.download` in the FlexForm, structured `download` object in the
|
||||
* JSON, rendered by ContentElementResolver inside the story content.
|
||||
*
|
||||
* Per text element with such a link (inside a usecase's content_elements):
|
||||
* - target download resolved by STORY TITLE (manual map first, then
|
||||
* download title minus "Success Story"/"Use Case"/"Usecase", minus
|
||||
* parentheticals and brand prefix, contains-tolerant; unique only) -
|
||||
* the stored uid is never trusted
|
||||
* - safety: converted only when the body is essentially just the button
|
||||
* (plain text aside from the link <= 60 chars); otherwise reported
|
||||
* - stories whose pdf is simply not in the pool: the button element is
|
||||
* HIDDEN instead of leaving a dead link (reported)
|
||||
*
|
||||
* Idempotent: converted elements no longer match the query.
|
||||
*
|
||||
* vendor/bin/typo3 vitec:convert-story-download-buttons
|
||||
* vendor/bin/typo3 vitec:convert-story-download-buttons --apply
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:convert-story-download-buttons',
|
||||
description: 'Convert story RTE download buttons into vitec_downloadcard plugins (dry-run by default)'
|
||||
)]
|
||||
final class ConvertStoryDownloadButtonsCommand extends Command
|
||||
{
|
||||
/** story-norm => exact download title (spelling variants, missing suffix) */
|
||||
private const MANUAL_MAP = [
|
||||
'isrvideomissioncommander' => 'ISR Mission Commander',
|
||||
'okadamanila' => 'VITEC Okada Manilla Usecase',
|
||||
];
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$apply = (bool)$input->getOption('apply');
|
||||
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$content = $pool->getConnectionForTable('tt_content');
|
||||
|
||||
$downloads = [];
|
||||
$allNorm = [];
|
||||
$uidByTitle = [];
|
||||
foreach ($pool->getConnectionForTable('tx_vitec_domain_model_download')->fetchAllAssociative(
|
||||
'SELECT uid, title FROM tx_vitec_domain_model_download WHERE deleted = 0'
|
||||
) as $download) {
|
||||
$uidByTitle[(string)$download['title']] = (int)$download['uid'];
|
||||
$allNorm[] = ['uid' => (int)$download['uid'], 'title' => (string)$download['title'], 'norm' => $this->norm((string)$download['title'])];
|
||||
$base = $this->stripSuffix((string)$download['title']);
|
||||
if ($base !== null) {
|
||||
$downloads[] = ['uid' => (int)$download['uid'], 'title' => (string)$download['title'], 'norm' => $this->norm($base)];
|
||||
}
|
||||
}
|
||||
|
||||
$elements = $content->fetchAllAssociative(
|
||||
"SELECT c.uid, c.bodytext, c.hidden, u.title AS story_title
|
||||
FROM tt_content c
|
||||
LEFT JOIN tx_vitec_domain_model_usecase u ON u.uid = c.tx_vitec_usecase_content AND u.deleted = 0
|
||||
WHERE c.deleted = 0 AND c.CType NOT IN ('vitec_downloadcard')
|
||||
AND c.bodytext LIKE '%identifier=download%'
|
||||
ORDER BY u.title, c.uid"
|
||||
);
|
||||
$output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . ' · ' . count($elements) . ' RTE download button(s)');
|
||||
|
||||
$datamap = [];
|
||||
$converted = 0;
|
||||
$hiddenCount = 0;
|
||||
$manual = [];
|
||||
foreach ($elements as $element) {
|
||||
$uid = (int)$element['uid'];
|
||||
$story = trim((string)($element['story_title'] ?? ''));
|
||||
$label = sprintf('#%d "%s"', $uid, $story !== '' ? $story : '- kein Usecase -');
|
||||
if ($story === '') {
|
||||
$manual[] = $label . ': Element haengt an keinem Usecase';
|
||||
continue;
|
||||
}
|
||||
// safety: the body must be essentially just the button
|
||||
$plain = trim((string)preg_replace('/\s+/', ' ', strip_tags((string)$element['bodytext'])));
|
||||
if (mb_strlen($plain) > 60) {
|
||||
$manual[] = $label . ': Bodytext enthaelt mehr als den Button (' . mb_strlen($plain) . ' Zeichen) - manuell';
|
||||
continue;
|
||||
}
|
||||
$target = $this->resolveDownload($story, $downloads, $uidByTitle, $allNorm);
|
||||
if ($target === null) {
|
||||
if ((int)$element['hidden'] === 1) {
|
||||
$output->writeln(' = ' . $label . ': kein PDF im Pool, Element bereits versteckt');
|
||||
continue;
|
||||
}
|
||||
$datamap['tt_content'][(string)$uid] = ['hidden' => 1];
|
||||
$hiddenCount++;
|
||||
$output->writeln(' - ' . $label . ': kein PDF im Pool -> Element wird VERSTECKT');
|
||||
continue;
|
||||
}
|
||||
$datamap['tt_content'][(string)$uid] = [
|
||||
'CType' => 'vitec_downloadcard',
|
||||
'header' => 'Downloads',
|
||||
'bodytext' => '',
|
||||
'pi_flexform' => [
|
||||
'data' => ['sDEF' => ['lDEF' => [
|
||||
'settings.download' => ['vDEF' => (string)$target['uid']],
|
||||
'settings.layout' => ['vDEF' => 'story_download'],
|
||||
]]],
|
||||
],
|
||||
];
|
||||
$converted++;
|
||||
$output->writeln(sprintf(' ~ %s -> vitec_downloadcard (download #%d "%s")', $label, $target['uid'], $target['title']));
|
||||
}
|
||||
|
||||
// follow-up pass: every story downloadcard gets the dedicated
|
||||
// "Success Story Download" layout (DataHandler merges the value
|
||||
// into the existing FlexForm) - covers cards converted before the
|
||||
// variant existed, including the interim `neutral` ones
|
||||
$layoutFixes = 0;
|
||||
$headerFixes = 0;
|
||||
foreach ($content->fetchAllAssociative(
|
||||
"SELECT uid, pi_flexform, header FROM tt_content
|
||||
WHERE deleted = 0 AND CType = 'vitec_downloadcard' AND tx_vitec_usecase_content > 0"
|
||||
) as $card) {
|
||||
$cardId = (string)(int)$card['uid'];
|
||||
if (!str_contains((string)$card['pi_flexform'], 'story_download')) {
|
||||
$datamap['tt_content'][$cardId]['pi_flexform'] =
|
||||
['data' => ['sDEF' => ['lDEF' => ['settings.layout' => ['vDEF' => 'story_download']]]]];
|
||||
$layoutFixes++;
|
||||
}
|
||||
// the front end builds the story anchor nav from element
|
||||
// HEADERS - without one the section is missing from it
|
||||
// (found 2026-10-28, "Downloads" anchor)
|
||||
if (trim((string)$card['header']) === '') {
|
||||
$datamap['tt_content'][$cardId]['header'] = 'Downloads';
|
||||
$headerFixes++;
|
||||
}
|
||||
}
|
||||
if ($layoutFixes > 0) {
|
||||
$output->writeln(' ~ ' . $layoutFixes . ' bestehende Story-Downloadcard(s): settings.layout -> story_download');
|
||||
}
|
||||
if ($headerFixes > 0) {
|
||||
$output->writeln(' ~ ' . $headerFixes . ' bestehende Story-Downloadcard(s): header -> "Downloads" (Anker-Nav)');
|
||||
}
|
||||
|
||||
$output->writeln(sprintf("\n%d Konvertierung(en), %d versteckt, %d manuell:", $converted, $hiddenCount, count($manual)));
|
||||
foreach ($manual as $line) {
|
||||
$output->writeln(' ! ' . $line);
|
||||
}
|
||||
if (!$apply) {
|
||||
$output->writeln('DRY RUN - nothing written. Add --apply to write.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
if ($datamap === []) {
|
||||
$output->writeln('Nothing to change.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start($datamap, []);
|
||||
$dataHandler->process_datamap();
|
||||
if ($dataHandler->errorLog !== []) {
|
||||
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$output->writeln('<info>Done. Flush the frontend cache, then re-check a story payload.</info>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{uid:int,title:string,norm:string}> $downloads
|
||||
* @param array<string,int> $uidByTitle
|
||||
* @return array{uid:int,title:string}|null unique match only
|
||||
*/
|
||||
private function resolveDownload(string $story, array $downloads, array $uidByTitle, array $allNorm = []): ?array
|
||||
{
|
||||
$storyNorm = $this->norm($story);
|
||||
$manualTitle = self::MANUAL_MAP[$storyNorm] ?? null;
|
||||
if ($manualTitle !== null && isset($uidByTitle[$manualTitle])) {
|
||||
return ['uid' => $uidByTitle[$manualTitle], 'title' => $manualTitle];
|
||||
}
|
||||
$candidates = [];
|
||||
foreach ($downloads as $download) {
|
||||
$a = $download['norm'];
|
||||
if ($a !== '' && $storyNorm !== '' && ($a === $storyNorm || str_contains($a, $storyNorm) || str_contains($storyNorm, $a))) {
|
||||
$candidates[] = $download;
|
||||
}
|
||||
}
|
||||
if (count($candidates) === 1) {
|
||||
return ['uid' => $candidates[0]['uid'], 'title' => $candidates[0]['title']];
|
||||
}
|
||||
// fallback: a download titled EXACTLY like the story, without any
|
||||
// document-type suffix ("Lord's Cricket Ground") - strict equality
|
||||
// only, so brochures and overviews cannot sneak in
|
||||
$exact = array_values(array_filter($allNorm, fn(array $d): bool => $d['norm'] === $storyNorm));
|
||||
return count($exact) === 1 ? ['uid' => $exact[0]['uid'], 'title' => $exact[0]['title']] : null;
|
||||
}
|
||||
|
||||
private function stripSuffix(string $title): ?string
|
||||
{
|
||||
foreach (['success story', 'use case', 'usecase'] as $suffix) {
|
||||
if (preg_match('/^(.*?)\s*' . preg_quote($suffix, '/') . '\s*$/i', trim($title), $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function norm(string $value): string
|
||||
{
|
||||
$value = (string)preg_replace('/\s*\([^)]*\)/', ' ', $value);
|
||||
$value = (string)preg_replace('/^\s*(vitec|datapath)\s+/i', '', $value);
|
||||
return (string)preg_replace('/[^a-z0-9]+/', '', str_replace(['+', '&'], ['plus', 'and'], mb_strtolower($value)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Command;
|
||||
|
||||
use Evomedien\Vitec\Import\ProductFamilyAliases;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Replaces the success-story "Featured Products" text sections with ONE
|
||||
* vitec_productlist plugin per story (decision Olli 2026-10-28).
|
||||
*
|
||||
* The story import left the section as plain text elements: a "Featured
|
||||
* Products" heading followed by product-teaser texts whose HEADERS carry
|
||||
* the OLD site's product names ("Avedia server c1520", "VITEC End-Points",
|
||||
* ...). Per story this command:
|
||||
*
|
||||
* - finds the heading text (header ~ "Featured Products") and collects
|
||||
* the consecutive text elements after it (same story, by sorting)
|
||||
* until the first non-text element or a text without header
|
||||
* - resolves each header to a product record: STORY_PRODUCT_ALIASES
|
||||
* first (old names), then exact title, title minus parenthesis,
|
||||
* ProductFamilyAliases, unique slug prefix
|
||||
* - creates ONE vitec_productlist in place of the heading (header
|
||||
* "Featured Products", FlexForm settings.selectedproducts = resolved
|
||||
* uids in section order, settings.layout = story_products - the NEW
|
||||
* "Success Story Products" layout) and DELETES the old text elements
|
||||
* - stories where NOTHING resolves keep their elements (reported);
|
||||
* partially resolved sections convert, the unresolved names are
|
||||
* reported for the alias map
|
||||
*
|
||||
* Idempotent: converted stories no longer have the heading text element.
|
||||
*
|
||||
* vendor/bin/typo3 vitec:convert-story-product-sections
|
||||
* vendor/bin/typo3 vitec:convert-story-product-sections --apply
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:convert-story-product-sections',
|
||||
description: 'Replace story "Featured Products" text sections with a productlist plugin (dry-run by default)'
|
||||
)]
|
||||
final class ConvertStoryProductSectionsCommand extends Command
|
||||
{
|
||||
/** old-site product names (normalised) => product slug */
|
||||
private const STORY_PRODUCT_ALIASES = [
|
||||
'avediaserverc1520' => 'avedia-platform-iptv-signage-cmsserver',
|
||||
'avediac1210chassis' => 'avedia-modular-chassis-c1101-c1103-c1210',
|
||||
'vitecendpoints' => 'avedia-end-points',
|
||||
];
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$apply = (bool)$input->getOption('apply');
|
||||
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$content = $pool->getConnectionForTable('tt_content');
|
||||
|
||||
$products = $pool->getConnectionForTable('tx_vitec_domain_model_product')->fetchAllAssociative(
|
||||
'SELECT uid, title, slug FROM tx_vitec_domain_model_product WHERE deleted = 0'
|
||||
);
|
||||
|
||||
$stories = $pool->getConnectionForTable('tx_vitec_domain_model_usecase')->fetchAllAssociative(
|
||||
'SELECT uid, title FROM tx_vitec_domain_model_usecase WHERE deleted = 0 ORDER BY title'
|
||||
);
|
||||
|
||||
$datamap = [];
|
||||
$cmdmap = [];
|
||||
$sortingFixes = [];
|
||||
$converted = 0;
|
||||
$deleted = 0;
|
||||
$unresolvedReport = [];
|
||||
$newIndex = 0;
|
||||
|
||||
foreach ($stories as $story) {
|
||||
$storyUid = (int)$story['uid'];
|
||||
$elements = $content->fetchAllAssociative(
|
||||
"SELECT uid, pid, CType, header, header_layout, colPos, sorting FROM tt_content
|
||||
WHERE deleted = 0 AND tx_vitec_usecase_content = ? AND tx_container_parent = 0
|
||||
ORDER BY sorting",
|
||||
[$storyUid]
|
||||
);
|
||||
// find the heading
|
||||
$startIndex = null;
|
||||
foreach ($elements as $i => $element) {
|
||||
if ($element['CType'] === 'text' && $this->norm((string)$element['header']) === 'featuredproducts') {
|
||||
$startIndex = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($startIndex === null) {
|
||||
continue;
|
||||
}
|
||||
$heading = $elements[$startIndex];
|
||||
// collect the teaser texts
|
||||
$section = [];
|
||||
for ($i = $startIndex + 1; $i < count($elements); $i++) {
|
||||
$element = $elements[$i];
|
||||
if ($element['CType'] !== 'text' || trim((string)$element['header']) === '') {
|
||||
break;
|
||||
}
|
||||
$section[] = $element;
|
||||
}
|
||||
if ($section === []) {
|
||||
$unresolvedReport[] = sprintf('"%s": Ueberschrift #%d ohne Produkt-Texte danach - uebersprungen', $story['title'], (int)$heading['uid']);
|
||||
continue;
|
||||
}
|
||||
// resolve products
|
||||
$uids = [];
|
||||
$unresolved = [];
|
||||
foreach ($section as $element) {
|
||||
$product = $this->resolveProduct((string)$element['header'], $products);
|
||||
if ($product !== null) {
|
||||
$uids[] = (int)$product['uid'];
|
||||
} else {
|
||||
$unresolved[] = (string)$element['header'];
|
||||
}
|
||||
}
|
||||
$uids = array_values(array_unique($uids));
|
||||
if ($uids === []) {
|
||||
$unresolvedReport[] = sprintf('"%s": KEIN Produkt aufloesbar (%s) - Elemente bleiben', $story['title'], implode(' | ', $unresolved));
|
||||
continue;
|
||||
}
|
||||
$output->writeln(sprintf('~ "%s": %d Text(e) -> productlist [%s]%s',
|
||||
$story['title'], count($section) + 1, implode(',', $uids),
|
||||
$unresolved === [] ? '' : ' UNAUFGELOEST: ' . implode(' | ', $unresolved)));
|
||||
if ($unresolved !== []) {
|
||||
$unresolvedReport[] = sprintf('"%s": teilweise - fehlend: %s', $story['title'], implode(' | ', $unresolved));
|
||||
}
|
||||
$newId = 'NEWSP' . (++$newIndex);
|
||||
$datamap['tt_content'][$newId] = [
|
||||
'pid' => (int)$heading['pid'],
|
||||
'CType' => 'vitec_productlist',
|
||||
'colPos' => (int)$heading['colPos'],
|
||||
'header' => (string)$heading['header'],
|
||||
'header_layout' => (int)$heading['header_layout'],
|
||||
'tx_vitec_usecase_content' => $storyUid,
|
||||
'pi_flexform' => [
|
||||
'data' => ['sDEF' => ['lDEF' => [
|
||||
'settings.selectedproducts' => ['vDEF' => implode(',', $uids)],
|
||||
'settings.layout' => ['vDEF' => 'story_products'],
|
||||
]]],
|
||||
],
|
||||
];
|
||||
$sortingFixes[$newId] = (int)$heading['sorting'];
|
||||
$cmdmap['tt_content'][(int)$heading['uid']]['delete'] = 1;
|
||||
foreach ($section as $element) {
|
||||
$cmdmap['tt_content'][(int)$element['uid']]['delete'] = 1;
|
||||
}
|
||||
$converted++;
|
||||
$deleted += count($section) + 1;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf("\n%d Story-Sektion(en) -> productlist, %d Element(e) werden geloescht, %d Hinweis(e):",
|
||||
$converted, $deleted, count($unresolvedReport)));
|
||||
foreach ($unresolvedReport as $line) {
|
||||
$output->writeln(' ! ' . $line);
|
||||
}
|
||||
if (!$apply) {
|
||||
$output->writeln('DRY RUN - nothing written. Add --apply to write.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
if ($datamap === [] && $cmdmap === []) {
|
||||
$output->writeln('Nothing to change.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start($datamap, $cmdmap);
|
||||
$dataHandler->process_datamap();
|
||||
$dataHandler->process_cmdmap();
|
||||
if ($dataHandler->errorLog !== []) {
|
||||
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
foreach ($sortingFixes as $newId => $sorting) {
|
||||
$uid = (int)($dataHandler->substNEWwithIDs[$newId] ?? 0);
|
||||
if ($uid > 0) {
|
||||
$content->update('tt_content', ['sorting' => $sorting], ['uid' => $uid]);
|
||||
}
|
||||
}
|
||||
$output->writeln('<info>Done. Flush the frontend cache, then re-check a story payload.</info>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $products */
|
||||
private function resolveProduct(string $header, array $products): ?array
|
||||
{
|
||||
$full = $this->norm($header);
|
||||
$alias = self::STORY_PRODUCT_ALIASES[$full] ?? null;
|
||||
if ($alias !== null) {
|
||||
foreach ($products as $product) {
|
||||
if ((string)$product['slug'] === $alias) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
$stripped = $this->norm((string)preg_replace('/\s*\(.*$/', '', $header));
|
||||
foreach ([$full, $stripped] as $needle) {
|
||||
foreach ($products as $product) {
|
||||
if ($this->norm((string)$product['title']) === $needle) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
}
|
||||
$family = ProductFamilyAliases::resolve($stripped);
|
||||
if ($family !== null) {
|
||||
return $family;
|
||||
}
|
||||
$matches = array_values(array_filter($products, fn(array $product): bool => str_starts_with(
|
||||
$this->norm((string)$product['slug']), $stripped
|
||||
)));
|
||||
return count($matches) === 1 ? $matches[0] : null;
|
||||
}
|
||||
|
||||
private function norm(string $value): string
|
||||
{
|
||||
return (string)preg_replace('/[^a-z0-9]+/', '', str_replace(['+', '&'], ['plus', 'and'], mb_strtolower($value)));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\DataProcessing;
|
||||
|
||||
use Evomedien\Vitec\Service\RteResolver;
|
||||
use Evomedien\Vitec\UserFunc\ProductListJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
||||
@@ -306,6 +307,13 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
||||
if (in_array($field, ['header_layout'], true)) {
|
||||
return (int)$value;
|
||||
}
|
||||
// RTE columns leave the DB with unresolved t3:// links; resolve them
|
||||
// like every other VITEC renderer does (found 2026-10-28: the
|
||||
// success-story "Download Success Story" buttons reached the payload
|
||||
// as raw t3://record hrefs).
|
||||
if (in_array($field, ['bodytext', 'vitec_quote'], true)) {
|
||||
return RteResolver::html($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -441,6 +441,13 @@ final class ContentElementResolver
|
||||
if (in_array($field, ['header_layout'], true)) {
|
||||
return (int)$value;
|
||||
}
|
||||
// RTE columns leave the DB with unresolved t3:// links; resolve them
|
||||
// like every other VITEC renderer does (found 2026-10-28: the
|
||||
// success-story "Download Success Story" buttons reached the payload
|
||||
// as raw t3://record hrefs).
|
||||
if (in_array($field, ['bodytext', 'vitec_quote'], true)) {
|
||||
return RteResolver::html($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,7 @@ class ProductListJsonRenderer
|
||||
$productsData[] = $this->serializeProduct($product);
|
||||
}
|
||||
|
||||
|
||||
// Envelope with layout and toolbar flag, matching the other list
|
||||
// plugins. This turned the payload from a bare array into an object:
|
||||
// the front end has to read `products.products` instead of
|
||||
@@ -221,6 +222,11 @@ class ProductListJsonRenderer
|
||||
// vocabulary the other lists use.
|
||||
$response = [
|
||||
'layout' => (string)($settings['layout'] ?? '0'),
|
||||
// grid hint (Olli 2026-10-28): the column count follows the
|
||||
// number of products, so a 2-product story section renders
|
||||
// 2 columns, a 3-product one 3 - the front end reads this
|
||||
// instead of guessing from the array length. Capped at 4.
|
||||
'columns' => min(4, max(1, count($productsData))),
|
||||
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||
'products' => $productsData,
|
||||
// Success stories the editor attached to these categories
|
||||
@@ -231,7 +237,12 @@ class ProductListJsonRenderer
|
||||
// 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(
|
||||
// Hand-picked lists (sub-market Products section, the story
|
||||
// "Featured Products" section) show EXACTLY the picked
|
||||
// products - no ride-along stories (2026-10-28: without this
|
||||
// the empty category selection aggregated stories over the
|
||||
// WHOLE tree and story cards leaked into the section).
|
||||
'successStories' => ($selectedUids !== [] && !$allProducts) ? [] : $this->successStoriesFor(
|
||||
($allProducts || $categoryUids === []) ? $this->allProductCategoryUids() : $categoryUids,
|
||||
(int)($settings['storysinglepid'] ?? 0)
|
||||
),
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
<label>Neutral Layout</label>
|
||||
<value>neutral</value>
|
||||
</numIndex>
|
||||
<numIndex index="3">
|
||||
<label>Success Story Download</label>
|
||||
<value>story_download</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
</config>
|
||||
</settings.layout>
|
||||
|
||||
@@ -99,6 +99,12 @@
|
||||
</label>
|
||||
<value>3</value>
|
||||
</numIndex>
|
||||
<numIndex index="4">
|
||||
<label>
|
||||
Success Story Products
|
||||
</label>
|
||||
<value>story_products</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
</config>
|
||||
</settings.layout>
|
||||
|
||||
Reference in new issue
Block a user