Datapath stories, grid repairs, menu v2 pure, hero placeholders, fec test
Batch of 2026-10-28/29 plus 2026-11-03: - NEW vitec:import-datapath-stories: 22 story packages from the agency dump (docx + Links/ assets) as usecase records with content, quotes, images, customer logos, download cards; --repair-quotes normalizes attribution lines; asset backfill for existing stories - /products grid incident repaired (fix_products_grid, fix_graphics_grid, check_grid_page): containers unhidden, 46 duplicate cards soft-deleted, section order restored; BuildProductPortfolio and ImportProductCategoryPages now GUARD against re-runs on pages that already carry vitec_cols_% containers - ' double-encoding fixed: render-pipeline importers write literal apostrophes (ENT_NOQUOTES), fix_double_entities literalizes existing rows - ticket quick wins (ticket_quickwins_20261028), broadcast market page cleanup, AttachCardImages market-card handling, SubmarketWorkbook META REPAIR, story content/video fixes (fix_story_*) - ProvideProductCatalog: transitional v1 fields removed (categories uid/link, legacyGroupFields, item products teaser) - pure v2 shape (ticket 0024-FE); search renderer keeps its teaser - BuildProductOverviews: heroes get the numbered placeholder background (placeholders/pages/<uid>-01.png, wide grey fallback) - backfill on existing pages with empty hero_bgimage, attached at creation for new pages; existing references always win (ticket 0026-FE); 40 placeholder PNGs shipped to fileadmin (not tracked) - AttachPageMedia: --depth=3 with sysfolders as pass-through parents for the overview pages' -08 menu tiles - NEW migrations/fec_isolation_test.sh: two-user live test proving fe_comment ticket isolation (backend verified tight, cross-user sighting traced to the FE page-JSON cache, ticket 0027-BE) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9211c8a395
commit
77661bf0b6
22 files changed
+2103
-63
No files matched your search
@@ -12,6 +12,7 @@ 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\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
@@ -29,9 +30,11 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
* Catalog reads it): editors rename, reorder or move overview pages there.
|
||||
* A and B are sysfolders - never routed, never in navigation menus. The 20
|
||||
* overview pages get flat slugs (decision Olli 2026-11-02) and, when they
|
||||
* are new or still empty, generated content: hero (page title), one intro
|
||||
* paragraph per grouping title (or "Product range"), a product card per
|
||||
* sub-product (record teaser, link /product/<slug>), contact pre-footer.
|
||||
* are new or still empty, generated content: hero (page title, placeholder
|
||||
* background - 0026-FE), one intro paragraph per grouping title (or
|
||||
* "Product range"), a product card per sub-product (record teaser, link
|
||||
* /product/<slug>), contact pre-footer. Existing pages whose hero carries
|
||||
* no image get the placeholder backfilled; any existing reference wins.
|
||||
* Afterwards run vitec:wrap-cards-grid per page and vitec:attach-card-images.
|
||||
*
|
||||
* Decisions without customer confirmation (Olli: proceed, nothing deleted):
|
||||
@@ -175,6 +178,7 @@ final class BuildProductOverviewsCommand extends Command
|
||||
/** @var string[] */
|
||||
private array $report = [];
|
||||
private int $newCounter = 0;
|
||||
private bool $apply = false;
|
||||
/** @var array<int,array{uid:int,title:string,slug:string,teaser:string}>|null */
|
||||
private ?array $products = null;
|
||||
|
||||
@@ -186,7 +190,7 @@ final class BuildProductOverviewsCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$apply = (bool)$input->getOption('apply');
|
||||
$apply = $this->apply = (bool)$input->getOption('apply');
|
||||
$pages = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
|
||||
$root = $pages->fetchAssociative("SELECT uid FROM pages WHERE deleted = 0 AND slug = '/products' AND sys_language_uid IN (0,-1)");
|
||||
if (!$root) {
|
||||
@@ -309,6 +313,8 @@ final class BuildProductOverviewsCommand extends Command
|
||||
$contentCount > 0 ? ' - content left untouched' : ' - content generated');
|
||||
if ($contentCount === 0) {
|
||||
$this->content($pageId, $entry, $cards);
|
||||
} else {
|
||||
$this->backfillHeroImage((int)$pageId);
|
||||
}
|
||||
} else {
|
||||
$pageId = $this->newId();
|
||||
@@ -373,10 +379,29 @@ final class BuildProductOverviewsCommand extends Command
|
||||
*/
|
||||
private function content(string $pageId, array $entry, array $cards): void
|
||||
{
|
||||
$this->element($pageId, self::COLPOS_HERO, 'vitec_herosection', [
|
||||
$heroId = $this->element($pageId, self::COLPOS_HERO, 'vitec_herosection', [
|
||||
'header' => (string)$entry['page'], 'header_layout' => 1,
|
||||
'bodytext' => isset($entry['desc']) ? $this->p(ucfirst((string)$entry['desc'])) : '',
|
||||
]);
|
||||
// no overview page goes live with an empty fullscreen hero (0026-FE):
|
||||
// the numbered placeholder when the page already has a uid, else the
|
||||
// generic wide grey one (swap the real asset in later)
|
||||
$placeholder = $this->heroPlaceholder($pageId);
|
||||
if ($placeholder === null) {
|
||||
$this->report[] = ' ! hero: no placeholder file in fileadmin - left empty';
|
||||
} elseif ($this->apply) {
|
||||
$refId = $this->newId();
|
||||
$this->datamap['sys_file_reference'][$refId] = [
|
||||
'pid' => $this->pid($pageId),
|
||||
'table_local' => 'sys_file',
|
||||
'uid_local' => GeneralUtility::makeInstance(StorageRepository::class)
|
||||
->getDefaultStorage()->getFile($placeholder)->getUid(),
|
||||
];
|
||||
$this->datamap['tt_content'][$heroId]['hero_bgimage'] = $refId;
|
||||
$this->report[] = ' hero: attach ' . $placeholder;
|
||||
} else {
|
||||
$this->report[] = ' hero: would attach ' . $placeholder;
|
||||
}
|
||||
if (!isset($cards[0]['group'])) {
|
||||
$this->element($pageId, self::COLPOS_MAIN, 'vitec_introparagraph', ['header' => 'Product range', 'bodytext' => '']);
|
||||
}
|
||||
@@ -412,11 +437,72 @@ final class BuildProductOverviewsCommand extends Command
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $fields */
|
||||
private function element(string $pageId, int $colPos, string $ctype, array $fields): void
|
||||
private function element(string $pageId, int $colPos, string $ctype, array $fields): string
|
||||
{
|
||||
$id = $this->newId();
|
||||
$this->datamap['tt_content'][$id] = ['pid' => $this->pid($pageId), 'colPos' => $colPos, 'CType' => $ctype] + $fields;
|
||||
$this->contentOrder[$pageId][$colPos][] = $id;
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the placeholder hero background to an EXISTING overview page
|
||||
* whose hero has no image yet (0026-FE: all 20 pages rendered an empty
|
||||
* fullscreen hero). A hero that carries ANY reference is never touched -
|
||||
* editors swap images in the element.
|
||||
*/
|
||||
private function backfillHeroImage(int $pageUid): void
|
||||
{
|
||||
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$heroUid = (int)$pool->getConnectionForTable('tt_content')->fetchOne(
|
||||
"SELECT uid FROM tt_content WHERE deleted = 0 AND pid = ? AND CType = 'vitec_herosection' ORDER BY colPos, sorting LIMIT 1",
|
||||
[$pageUid]
|
||||
);
|
||||
if ($heroUid === 0) {
|
||||
return;
|
||||
}
|
||||
$existing = (int)$pool->getConnectionForTable('sys_file_reference')->fetchOne(
|
||||
"SELECT COUNT(*) FROM sys_file_reference WHERE deleted = 0 AND tablenames = 'tt_content' AND fieldname = 'hero_bgimage' AND uid_foreign = ?",
|
||||
[$heroUid]
|
||||
);
|
||||
if ($existing > 0) {
|
||||
return;
|
||||
}
|
||||
$placeholder = $this->heroPlaceholder((string)$pageUid);
|
||||
if ($placeholder === null) {
|
||||
$this->report[] = ' ! hero #' . $heroUid . ': empty and no placeholder file - left empty';
|
||||
return;
|
||||
}
|
||||
if (!$this->apply) {
|
||||
$this->report[] = ' hero #' . $heroUid . ': empty, would attach ' . $placeholder;
|
||||
return;
|
||||
}
|
||||
$refId = $this->newId();
|
||||
$this->datamap['sys_file_reference'][$refId] = [
|
||||
'pid' => $pageUid,
|
||||
'table_local' => 'sys_file',
|
||||
'uid_local' => GeneralUtility::makeInstance(StorageRepository::class)
|
||||
->getDefaultStorage()->getFile($placeholder)->getUid(),
|
||||
'tablenames' => 'tt_content',
|
||||
'uid_foreign' => $heroUid,
|
||||
'fieldname' => 'hero_bgimage',
|
||||
];
|
||||
$this->datamap['tt_content'][$heroUid]['hero_bgimage'] = 1;
|
||||
$this->report[] = ' hero #' . $heroUid . ': attach ' . $placeholder;
|
||||
}
|
||||
|
||||
/** The hero placeholder identifier for a page: numbered when available, generic wide grey otherwise, null when neither file exists. */
|
||||
private function heroPlaceholder(string $pageId): ?string
|
||||
{
|
||||
$storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage();
|
||||
$candidates = ctype_digit($pageId) ? ['placeholders/pages/' . $pageId . '-01.png'] : [];
|
||||
$candidates[] = 'placeholders/placeholder-grey-wide.png';
|
||||
foreach ($candidates as $identifier) {
|
||||
if ($storage->hasFile($identifier)) {
|
||||
return $identifier;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- lookups -----------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user