Unify list plugins, add solution list and per-story detail page
- vitec_usecaselist: layout + record selection like marketlist - new plugin vitec_solutionlist (spec 7.13.3, key "solutions") - tx_vitec_domain_model_usecase: detail_page -> resolved detailUrl (also gives story cards in vitec_modelcard a link for the first time) - "Show Toolbar" checkbox on product/market/solution/usecase lists - spec bumped to v1.9 BREAKING: vitec_productlist and vitec_usecaselist now emit an object instead of a bare array. Consumers must read products.products and usecases.usecases. Also found: productlist had a configurable layout that was never serialised; it is emitted now, but keeps its own 0-3 vocabulary instead of grid/list/carousel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,19 @@ class SolutionController extends ActionController
|
|||||||
*
|
*
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* action list
|
||||||
|
*
|
||||||
|
* Intentionally empty: in headless mode the JSON is produced by
|
||||||
|
* SolutionListJsonRenderer through TypoScript, so this action never runs.
|
||||||
|
* It exists because configurePlugin() needs a target - same as
|
||||||
|
* MarketController::listAction().
|
||||||
|
*/
|
||||||
|
public function listAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->htmlResponse();
|
||||||
|
}
|
||||||
|
|
||||||
public function showAction(): ResponseInterface
|
public function showAction(): ResponseInterface
|
||||||
{
|
{
|
||||||
$selectedSolutionId = (int)$this->settings['solution'];
|
$selectedSolutionId = (int)$this->settings['solution'];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
|||||||
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\MarketListJsonRenderer;
|
use Evomedien\Vitec\UserFunc\MarketListJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\SolutionListJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
|
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
|
||||||
@@ -119,6 +120,7 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
||||||
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
|
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
|
||||||
'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'],
|
'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'],
|
||||||
|
'vitec_solutionlist' => [SolutionListJsonRenderer::class, 'solutions'],
|
||||||
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
|
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
|
||||||
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
|
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
|
||||||
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
|
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ class Usecase extends AbstractEntity
|
|||||||
*/
|
*/
|
||||||
protected $slug = '';
|
protected $slug = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that presents this story. 0 = none; the list plugin then falls back
|
||||||
|
* to its own "Single PID" detail page plus the slug.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $detailPage = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* subtitle
|
* subtitle
|
||||||
*
|
*
|
||||||
@@ -110,6 +118,16 @@ class Usecase extends AbstractEntity
|
|||||||
return $this->slug;
|
return $this->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDetailPage(): int
|
||||||
|
{
|
||||||
|
return $this->detailPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDetailPage(int $detailPage): void
|
||||||
|
{
|
||||||
|
$this->detailPage = $detailPage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $slug
|
* @param string $slug
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer;
|
|||||||
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\MarketListJsonRenderer;
|
use Evomedien\Vitec\UserFunc\MarketListJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\SolutionListJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
|
use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
|
use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer;
|
||||||
@@ -77,6 +78,7 @@ final class ContentElementResolver
|
|||||||
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'],
|
||||||
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
|
'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'],
|
||||||
'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'],
|
'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'],
|
||||||
|
'vitec_solutionlist' => [SolutionListJsonRenderer::class, 'solutions'],
|
||||||
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
|
'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'],
|
||||||
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
|
'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'],
|
||||||
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
|
'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Evomedien\Vitec\Service;
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
use Doctrine\DBAL\ParameterType;
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use Evomedien\Vitec\Service\LinkResolver;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
@@ -42,6 +43,10 @@ final class UsecaseSerializer
|
|||||||
'uid' => $uid,
|
'uid' => $uid,
|
||||||
'title' => (string)($u['title'] ?? ''),
|
'title' => (string)($u['title'] ?? ''),
|
||||||
'slug' => (string)($u['slug'] ?? ''),
|
'slug' => (string)($u['slug'] ?? ''),
|
||||||
|
// The story's own detail page, when one is set. null means "none",
|
||||||
|
// and the list plugin then falls back to its Single PID plus the
|
||||||
|
// slug - which is how every story behaved before the field existed.
|
||||||
|
'detailUrl' => LinkResolver::pageUrl((int)($u['detail_page'] ?? 0)),
|
||||||
'subtitle' => (string)($u['subtitle'] ?? ''),
|
'subtitle' => (string)($u['subtitle'] ?? ''),
|
||||||
'teaser' => (string)($u['teaser'] ?? ''),
|
'teaser' => (string)($u['teaser'] ?? ''),
|
||||||
'cardImage' => $this->image($uid, 'card_image', null, true),
|
'cardImage' => $this->image($uid, 'card_image', null, true),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
|||||||
* UserFunc: render all VITEC markets as JSON (headless).
|
* UserFunc: render all VITEC markets as JSON (headless).
|
||||||
*
|
*
|
||||||
* Output under content.markets:
|
* Output under content.markets:
|
||||||
* { "layout": "grid|list|carousel", "markets": [ … ] }
|
* { "layout": "grid|list|carousel|50-50", "markets": [ … ] }
|
||||||
*
|
*
|
||||||
* Selection semantics (FlexForm `settings.markets`):
|
* Selection semantics (FlexForm `settings.markets`):
|
||||||
* - nothing selected -> ALL visible markets, alphabetical by title
|
* - nothing selected -> ALL visible markets, alphabetical by title
|
||||||
@@ -152,6 +152,7 @@ class MarketListJsonRenderer
|
|||||||
|
|
||||||
$response = [
|
$response = [
|
||||||
'layout' => $layout,
|
'layout' => $layout,
|
||||||
|
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||||
'markets' => $markets,
|
'markets' => $markets,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -152,20 +152,30 @@ class ProductListJsonRenderer
|
|||||||
$productsData[] = $this->serializeProduct($product);
|
$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
|
||||||
|
// iterating `products` directly. `layout` was configurable in the
|
||||||
|
// FlexForm all along but had never been emitted - note that product
|
||||||
|
// layouts are numbered 0..3 here, not the grid/list/carousel
|
||||||
|
// vocabulary the other lists use.
|
||||||
|
$response = [
|
||||||
|
'layout' => (string)($settings['layout'] ?? '0'),
|
||||||
|
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||||
|
'products' => $productsData,
|
||||||
|
];
|
||||||
|
|
||||||
if ($debugMode) {
|
if ($debugMode) {
|
||||||
return json_encode([
|
$response['debug'] = [
|
||||||
'products' => $productsData,
|
'pageId' => (int)($GLOBALS['TSFE']->id ?? 0),
|
||||||
'debug' => [
|
'categoryUids' => $categoryUids,
|
||||||
'pageId' => (int)($GLOBALS['TSFE']->id ?? 0),
|
'productCount' => count($productsData),
|
||||||
'categoryUids' => $categoryUids,
|
'settings' => $settings,
|
||||||
'productCount' => count($productsData),
|
'fallbackFiles' => $this->collectFallbackFilesFromProducts($productsData),
|
||||||
'settings' => $settings,
|
];
|
||||||
'fallbackFiles' => $this->collectFallbackFilesFromProducts($productsData),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return json_encode($productsData);
|
return json_encode($response);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
196
packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php
Normal file
196
packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use Evomedien\Vitec\Service\LinkResolver;
|
||||||
|
use Evomedien\Vitec\Service\RteResolver;
|
||||||
|
use Evomedien\Vitec\Service\UsecaseSerializer;
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc: render all VITEC solutions as JSON (headless).
|
||||||
|
*
|
||||||
|
* Output under content.solutions:
|
||||||
|
* { "layout": "grid|list|carousel|50-50", "solutions": [ … ] }
|
||||||
|
*
|
||||||
|
* This is the Solution counterpart the architecture spec asks for in clause
|
||||||
|
* 7.13.2 - deliberately the same shape as the market list, so the front end can
|
||||||
|
* serve both with one component.
|
||||||
|
*
|
||||||
|
* Selection semantics (FlexForm `settings.solutions`):
|
||||||
|
* - nothing selected -> ALL visible solutions, alphabetical by title
|
||||||
|
* (tx_vitec_domain_model_solution has no `sorting` column, same as market)
|
||||||
|
* - solutions selected -> exactly those, in the order the editor arranged
|
||||||
|
* them in the FlexForm
|
||||||
|
*
|
||||||
|
* The page carrying the plugin is never used as a filter.
|
||||||
|
*
|
||||||
|
* Each item uses the same shape as the card payload (ModelcardJsonRenderer,
|
||||||
|
* model type "solution") and carries `detailUrl`, resolved from the record's
|
||||||
|
* own `detail_page`. Image resolution is delegated to
|
||||||
|
* UsecaseSerializer::image() rather than re-implemented inline (clause 9.2).
|
||||||
|
*
|
||||||
|
* `render()` = top-level plugin / page discovery. `renderForRecord()` = one
|
||||||
|
* specific tt_content row (reused by ContainerChildrenProcessor for nested
|
||||||
|
* plugins). Exception-safe.
|
||||||
|
*/
|
||||||
|
class SolutionListJsonRenderer
|
||||||
|
{
|
||||||
|
private ?ContentObjectRenderer $cObj = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
|
||||||
|
* ContentObjectRenderer::callUserFunction() duck-types it with
|
||||||
|
* is_callable([$classObj, 'setContentObjectRenderer']). Without the method
|
||||||
|
* $this->cObj stays null, the cObj branch of render() never fires and the
|
||||||
|
* call falls through to page discovery, which picks the FIRST element of
|
||||||
|
* this CType on the page rather than the one actually being rendered.
|
||||||
|
*/
|
||||||
|
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
|
||||||
|
{
|
||||||
|
$this->cObj = $cObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const TABLE = 'tx_vitec_domain_model_solution';
|
||||||
|
private const CTYPE = 'vitec_solutionlist';
|
||||||
|
|
||||||
|
#[AsAllowedCallable]
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
|
||||||
|
if ($row && (string)($row['CType'] ?? '') === self::CTYPE) {
|
||||||
|
return $this->renderForRecord($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageId = 0;
|
||||||
|
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||||
|
if ($request !== null) {
|
||||||
|
$pageInfo = $request->getAttribute('frontend.page.information');
|
||||||
|
if ($pageInfo !== null) {
|
||||||
|
$pageId = (int)$pageInfo->getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($pageId <= 0) {
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
}
|
||||||
|
if ($pageId <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||||
|
$ces = $qb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$qb->expr()->eq('CType', $qb->createNamedParameter(self::CTYPE, ParameterType::STRING)),
|
||||||
|
$qb->expr()->eq('deleted', 0),
|
||||||
|
$qb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($ces)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderForRecord($ces[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $contentElement
|
||||||
|
*/
|
||||||
|
public function renderForRecord(array $contentElement): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
|
||||||
|
$layout = (string)($settings['layout'] ?? 'grid');
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
$selectedUids = GeneralUtility::intExplode(',', (string)($settings['solutions'] ?? ''), true);
|
||||||
|
|
||||||
|
// Always fetch the full (small) table once - that way a selected
|
||||||
|
// record that has meanwhile been hidden or deleted simply drops out.
|
||||||
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE);
|
||||||
|
$rows = $qb
|
||||||
|
->select('*')
|
||||||
|
->from(self::TABLE)
|
||||||
|
->where(
|
||||||
|
$qb->expr()->eq('deleted', 0),
|
||||||
|
$qb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('title', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$serializer = GeneralUtility::makeInstance(UsecaseSerializer::class);
|
||||||
|
|
||||||
|
if ($selectedUids === []) {
|
||||||
|
$solutions = array_map(
|
||||||
|
fn(array $r): array => $this->serializeSolution($r, $serializer),
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Keep the editor's FlexForm order. A SQL IN() would return the
|
||||||
|
// rows in storage order, so the sequence is rebuilt here.
|
||||||
|
$byUid = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$byUid[(int)$r['uid']] = $r;
|
||||||
|
}
|
||||||
|
$solutions = [];
|
||||||
|
foreach ($selectedUids as $uid) {
|
||||||
|
if (isset($byUid[$uid])) {
|
||||||
|
$solutions[] = $this->serializeSolution($byUid[$uid], $serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'layout' => $layout,
|
||||||
|
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||||
|
'solutions' => $solutions,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = [
|
||||||
|
'count' => count($solutions),
|
||||||
|
'selected' => $selectedUids,
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)json_encode($response);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $r
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function serializeSolution(array $r, UsecaseSerializer $serializer): array
|
||||||
|
{
|
||||||
|
$uid = (int)$r['uid'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $uid,
|
||||||
|
'title' => (string)($r['title'] ?? ''),
|
||||||
|
'slug' => (string)($r['slug'] ?? ''),
|
||||||
|
'subtitle' => (string)($r['subtitle'] ?? ''),
|
||||||
|
'teaser' => (string)($r['teaser'] ?? ''),
|
||||||
|
'description' => RteResolver::html($r['description'] ?? ''),
|
||||||
|
'detailUrl' => LinkResolver::pageUrl((int)($r['detail_page'] ?? 0)),
|
||||||
|
'image' => $serializer->image($uid, 'image', self::TABLE, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,11 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
|||||||
* specific tt_content row (reused by ContainerChildrenProcessor for nested
|
* specific tt_content row (reused by ContainerChildrenProcessor for nested
|
||||||
* usecase-list plugins). All serialisation is delegated to UsecaseSerializer.
|
* usecase-list plugins). All serialisation is delegated to UsecaseSerializer.
|
||||||
* Exception-safe.
|
* Exception-safe.
|
||||||
|
*
|
||||||
|
* Payload: { "layout": "grid|list|carousel|50-50", "usecases": [ … ] }
|
||||||
|
*
|
||||||
|
* The editor may pick individual stories in the FlexForm; the arranged order is
|
||||||
|
* the display order. No selection means all stories, featured first.
|
||||||
*/
|
*/
|
||||||
class UsecaseListJsonRenderer
|
class UsecaseListJsonRenderer
|
||||||
{
|
{
|
||||||
@@ -109,6 +114,8 @@ class UsecaseListJsonRenderer
|
|||||||
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
$settings = $flexFormData['settings'] ?? [];
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
$debugMode = (bool)($settings['debug'] ?? false);
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
$layout = (string)($settings['layout'] ?? 'grid');
|
||||||
|
$selectedUids = GeneralUtility::intExplode(',', (string)($settings['usecases'] ?? ''), true);
|
||||||
|
|
||||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE);
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(self::TABLE);
|
||||||
$rows = $qb
|
$rows = $qb
|
||||||
@@ -139,25 +146,59 @@ class UsecaseListJsonRenderer
|
|||||||
$detailBase = $parent !== '' ? $parent : $detailPath;
|
$detailBase = $parent !== '' ? $parent : $detailPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Editor-picked stories keep the order they were arranged in. The
|
||||||
|
// full (small) table is fetched once and the sequence rebuilt here:
|
||||||
|
// a SQL IN() would hand the rows back in storage order, and a story
|
||||||
|
// that has meanwhile been hidden simply drops out. Same approach as
|
||||||
|
// MarketListJsonRenderer.
|
||||||
|
if ($selectedUids !== []) {
|
||||||
|
$byUid = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$byUid[(int)$r['uid']] = $r;
|
||||||
|
}
|
||||||
|
$ordered = [];
|
||||||
|
foreach ($selectedUids as $uid) {
|
||||||
|
if (isset($byUid[$uid])) {
|
||||||
|
$ordered[] = $byUid[$uid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$rows = $ordered;
|
||||||
|
}
|
||||||
|
|
||||||
$usecases = array_map(
|
$usecases = array_map(
|
||||||
static function (array $u) use ($serializer, $detailBase): array {
|
static function (array $u) use ($serializer, $detailBase): array {
|
||||||
$item = $serializer->serializeListItem($u);
|
$item = $serializer->serializeListItem($u);
|
||||||
$item['detailUrl'] = ($detailBase !== '' && ($item['slug'] ?? '') !== '')
|
// A story with its own detail page wins; only the others
|
||||||
? $detailBase . '/' . ltrim((string)$item['slug'], '/')
|
// fall back to the Single PID plus the slug, so nothing
|
||||||
: null;
|
// changes for the stories that have no page assigned.
|
||||||
|
if (($item['detailUrl'] ?? null) === null) {
|
||||||
|
$item['detailUrl'] = ($detailBase !== '' && ($item['slug'] ?? '') !== '')
|
||||||
|
? $detailBase . '/' . ltrim((string)$item['slug'], '/')
|
||||||
|
: null;
|
||||||
|
}
|
||||||
return $item;
|
return $item;
|
||||||
},
|
},
|
||||||
$rows
|
$rows
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Envelope with the layout, matching vitec_marketlist. This changed
|
||||||
|
// the payload from a bare array to an object - the front end has to
|
||||||
|
// read `usecases.usecases` instead of iterating `usecases` directly.
|
||||||
|
$response = [
|
||||||
|
'layout' => $layout,
|
||||||
|
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||||
|
'usecases' => $usecases,
|
||||||
|
];
|
||||||
|
|
||||||
if ($debugMode) {
|
if ($debugMode) {
|
||||||
return (string)json_encode([
|
$response['debug'] = [
|
||||||
'usecases' => $usecases,
|
'count' => count($usecases),
|
||||||
'debug' => ['count' => count($usecases), 'settings' => $settings],
|
'selected' => $selectedUids,
|
||||||
]);
|
'settings' => $settings,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string)json_encode($usecases);
|
return (string)json_encode($response);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,24 @@
|
|||||||
<label>Carousel</label>
|
<label>Carousel</label>
|
||||||
<value>carousel</value>
|
<value>carousel</value>
|
||||||
</numIndex>
|
</numIndex>
|
||||||
|
<numIndex index="3">
|
||||||
|
<label>50 / 50</label>
|
||||||
|
<value>50-50</value>
|
||||||
|
</numIndex>
|
||||||
</items>
|
</items>
|
||||||
<default>grid</default>
|
<default>grid</default>
|
||||||
</config>
|
</config>
|
||||||
</settings.layout>
|
</settings.layout>
|
||||||
|
|
||||||
|
<settings.showtoolbar>
|
||||||
|
<label>Show Toolbar</label>
|
||||||
|
<description>Show the toolbar above the list.</description>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.showtoolbar>
|
||||||
|
|
||||||
<settings.markets>
|
<settings.markets>
|
||||||
<label>Markets</label>
|
<label>Markets</label>
|
||||||
<description>Leave empty to show all markets alphabetically. If you select markets, exactly those are shown — in the order you arrange them here (use the arrows to sort).</description>
|
<description>Leave empty to show all markets alphabetically. If you select markets, exactly those are shown — in the order you arrange them here (use the arrows to sort).</description>
|
||||||
|
|||||||
@@ -92,6 +92,14 @@
|
|||||||
</items>
|
</items>
|
||||||
</config>
|
</config>
|
||||||
</settings.layout>
|
</settings.layout>
|
||||||
|
<settings.showtoolbar>
|
||||||
|
<label>Show Toolbar</label>
|
||||||
|
<description>Show the toolbar above the list.</description>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.showtoolbar>
|
||||||
</el>
|
</el>
|
||||||
</ROOT>
|
</ROOT>
|
||||||
</sDEF>
|
</sDEF>
|
||||||
|
|||||||
71
packages/vitec/Configuration/FlexForms/Solutionlist.xml
Normal file
71
packages/vitec/Configuration/FlexForms/Solutionlist.xml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<T3DataStructure>
|
||||||
|
<sheets>
|
||||||
|
<sDEF>
|
||||||
|
<ROOT>
|
||||||
|
<sheetTitle>Solution List Settings</sheetTitle>
|
||||||
|
<type>array</type>
|
||||||
|
<el>
|
||||||
|
<settings.layout>
|
||||||
|
<label>Layout</label>
|
||||||
|
<description>Display variant for the solution cards.</description>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectSingle</renderType>
|
||||||
|
<items>
|
||||||
|
<numIndex index="0">
|
||||||
|
<label>Grid</label>
|
||||||
|
<value>grid</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="1">
|
||||||
|
<label>List</label>
|
||||||
|
<value>list</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="2">
|
||||||
|
<label>Carousel</label>
|
||||||
|
<value>carousel</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="3">
|
||||||
|
<label>50 / 50</label>
|
||||||
|
<value>50-50</value>
|
||||||
|
</numIndex>
|
||||||
|
</items>
|
||||||
|
<default>grid</default>
|
||||||
|
</config>
|
||||||
|
</settings.layout>
|
||||||
|
|
||||||
|
<settings.showtoolbar>
|
||||||
|
<label>Show Toolbar</label>
|
||||||
|
<description>Show the toolbar above the list.</description>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.showtoolbar>
|
||||||
|
|
||||||
|
<settings.solutions>
|
||||||
|
<label>Solutions</label>
|
||||||
|
<description>Leave empty to show all solutions alphabetically. If you select solutions, exactly those are shown — in the order you arrange them here (use the arrows to sort).</description>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectMultipleSideBySide</renderType>
|
||||||
|
<foreign_table>tx_vitec_domain_model_solution</foreign_table>
|
||||||
|
<foreign_table_where>AND tx_vitec_domain_model_solution.hidden = 0 AND tx_vitec_domain_model_solution.deleted = 0 ORDER BY tx_vitec_domain_model_solution.title</foreign_table_where>
|
||||||
|
<size>8</size>
|
||||||
|
<minitems>0</minitems>
|
||||||
|
<maxitems>999</maxitems>
|
||||||
|
</config>
|
||||||
|
</settings.solutions>
|
||||||
|
|
||||||
|
<settings.debug>
|
||||||
|
<label>Allow Debug Output</label>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.debug>
|
||||||
|
</el>
|
||||||
|
</ROOT>
|
||||||
|
</sDEF>
|
||||||
|
</sheets>
|
||||||
|
</T3DataStructure>
|
||||||
@@ -6,6 +6,57 @@
|
|||||||
<sheetTitle>Usecase List</sheetTitle>
|
<sheetTitle>Usecase List</sheetTitle>
|
||||||
<type>array</type>
|
<type>array</type>
|
||||||
<el>
|
<el>
|
||||||
|
<settings.layout>
|
||||||
|
<label>Layout</label>
|
||||||
|
<description>Display variant for the success story cards.</description>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectSingle</renderType>
|
||||||
|
<items>
|
||||||
|
<numIndex index="0">
|
||||||
|
<label>Grid</label>
|
||||||
|
<value>grid</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="1">
|
||||||
|
<label>List</label>
|
||||||
|
<value>list</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="2">
|
||||||
|
<label>Carousel</label>
|
||||||
|
<value>carousel</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="3">
|
||||||
|
<label>50 / 50</label>
|
||||||
|
<value>50-50</value>
|
||||||
|
</numIndex>
|
||||||
|
</items>
|
||||||
|
<default>grid</default>
|
||||||
|
</config>
|
||||||
|
</settings.layout>
|
||||||
|
|
||||||
|
<settings.showtoolbar>
|
||||||
|
<label>Show Toolbar</label>
|
||||||
|
<description>Show the toolbar above the list.</description>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.showtoolbar>
|
||||||
|
|
||||||
|
<settings.usecases>
|
||||||
|
<label>Success Stories</label>
|
||||||
|
<description>Leave empty to show all stories (featured first, then alphabetically). If you select stories, exactly those are shown — in the order you arrange them here (use the arrows to sort).</description>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectMultipleSideBySide</renderType>
|
||||||
|
<foreign_table>tx_vitec_domain_model_usecase</foreign_table>
|
||||||
|
<foreign_table_where>AND tx_vitec_domain_model_usecase.hidden = 0 AND tx_vitec_domain_model_usecase.deleted = 0 AND tx_vitec_domain_model_usecase.hideonwebsite = 0 ORDER BY tx_vitec_domain_model_usecase.title</foreign_table_where>
|
||||||
|
<size>8</size>
|
||||||
|
<minitems>0</minitems>
|
||||||
|
<maxitems>999</maxitems>
|
||||||
|
</config>
|
||||||
|
</settings.usecases>
|
||||||
|
|
||||||
<settings.singlePid>
|
<settings.singlePid>
|
||||||
<label>Single PID (detail page)</label>
|
<label>Single PID (detail page)</label>
|
||||||
<description>Page that holds the "Show single VITEC Success Story" plugin. Used to build the detail links in the JSON output.</description>
|
<description>Page that holds the "Show single VITEC Success Story" plugin. Used to build the detail links in the JSON output.</description>
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ return [
|
|||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-marketlist.svg',
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-marketlist.svg',
|
||||||
],
|
],
|
||||||
|
'vitec-plugin-solutionlist' => [
|
||||||
|
'provider' => SvgIconProvider::class,
|
||||||
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-solutionlist.svg',
|
||||||
|
],
|
||||||
'vitec-import' => [
|
'vitec-import' => [
|
||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-import.svg',
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-import.svg',
|
||||||
|
|||||||
@@ -113,6 +113,18 @@ tt_content {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vitec_solutionlist < lib.contentElementWithHeader
|
||||||
|
vitec_solutionlist {
|
||||||
|
fields {
|
||||||
|
content {
|
||||||
|
fields {
|
||||||
|
solutions = USER
|
||||||
|
solutions.userFunc = Evomedien\Vitec\UserFunc\SolutionListJsonRenderer->render
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vitec_solutionshow < lib.contentElementWithHeader
|
vitec_solutionshow < lib.contentElementWithHeader
|
||||||
vitec_solutionshow {
|
vitec_solutionshow {
|
||||||
fields {
|
fields {
|
||||||
|
|||||||
@@ -111,6 +111,16 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
|||||||
'FILE:EXT:vitec/Configuration/FlexForms/Marketlist.xml'
|
'FILE:EXT:vitec/Configuration/FlexForms/Marketlist.xml'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ExtensionUtility::registerPlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Solutionlist',
|
||||||
|
'VITEC Solution List',
|
||||||
|
'vitec-plugin-solutionlist',
|
||||||
|
'vitec',
|
||||||
|
'All solutions as grid, list or carousel.',
|
||||||
|
'FILE:EXT:vitec/Configuration/FlexForms/Solutionlist.xml'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// Change frame_class to allow multiple selections.
|
// Change frame_class to allow multiple selections.
|
||||||
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
|
$GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox';
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ return [
|
|||||||
'1' => [
|
'1' => [
|
||||||
'showitem' =>
|
'showitem' =>
|
||||||
'--div--;General,
|
'--div--;General,
|
||||||
title, slug, subtitle,
|
title, slug, detail_page, subtitle,
|
||||||
--div--;List view,
|
--div--;List view,
|
||||||
card_image, customer_logo, teaser,
|
card_image, customer_logo, teaser,
|
||||||
--div--;Detail · Hero,
|
--div--;Detail · Hero,
|
||||||
@@ -109,6 +109,17 @@ return [
|
|||||||
'eval' => 'uniqueInPid',
|
'eval' => 'uniqueInPid',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'detail_page' => [
|
||||||
|
'exclude' => true,
|
||||||
|
'label' => 'Detail Page',
|
||||||
|
'description' => 'Page that presents this story. Resolved to a URL in the JSON output; leave empty to keep using the list plugin\'s Single PID plus the slug.',
|
||||||
|
'config' => [
|
||||||
|
'type' => 'group',
|
||||||
|
'allowed' => 'pages',
|
||||||
|
'size' => 1,
|
||||||
|
'maxitems' => 1,
|
||||||
|
],
|
||||||
|
],
|
||||||
'subtitle' => [
|
'subtitle' => [
|
||||||
'label' => 'Subtitle',
|
'label' => 'Subtitle',
|
||||||
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''],
|
'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''],
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Document identifier** | EVO‑VITEC‑HL‑001 |
|
| **Document identifier** | EVO‑VITEC‑HL‑001 |
|
||||||
| **Version** | 1.8 |
|
| **Version** | 1.9 |
|
||||||
| **Status** | Released |
|
| **Status** | Released |
|
||||||
| **Date** | 2026‑08‑13 |
|
| **Date** | 2026‑08‑15 |
|
||||||
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
|
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
|
||||||
| **Owner** | evomedien — VITEC relaunch |
|
| **Owner** | evomedien — VITEC relaunch |
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
| 1.6 | 2026‑08‑06 | **Interface change (additive):** `tx_vitec_domain_model_download` gained a second category field `type` (display taxonomy; MM rows distinguished by `fieldname`), emitted as the string `type` in the downloadcard, downloadcardcollection and datasheets payloads — analogous to `filetype`. New CLI command `vitec:import-downloads` migrates the old‑site downloads (Collateral directory only; idempotent by slug; files fetched resumably; duplicate `file_url`s merged). |
|
| 1.6 | 2026‑08‑06 | **Interface change (additive):** `tx_vitec_domain_model_download` gained a second category field `type` (display taxonomy; MM rows distinguished by `fieldname`), emitted as the string `type` in the downloadcard, downloadcardcollection and datasheets payloads — analogous to `filetype`. New CLI command `vitec:import-downloads` migrates the old‑site downloads (Collateral directory only; idempotent by slug; files fetched resumably; duplicate `file_url`s merged). |
|
||||||
| 1.7 | 2026‑08‑06 | **Robustness:** the import normalizes legacy filenames on fetch so every imported file matches the version convention (`__NN_A` → `__NN-A`, `___NN` → `__NN`, `__NNA` → `__NN-A`, bare `__NN` → `__NN-A` as initial revision), and the three download renderers gained a `filepath` fallback in `getDownloadFile()` (FAL → convention → filepath) as a safety net for anything that still escapes it. Extends the B‑4 duplication (three copies of the fallback) — consolidation target remains a shared file‑resolver service (10.2). |
|
| 1.7 | 2026‑08‑06 | **Robustness:** the import normalizes legacy filenames on fetch so every imported file matches the version convention (`__NN_A` → `__NN-A`, `___NN` → `__NN`, `__NNA` → `__NN-A`, bare `__NN` → `__NN-A` as initial revision), and the three download renderers gained a `filepath` fallback in `getDownloadFile()` (FAL → convention → filepath) as a safety net for anything that still escapes it. Extends the B‑4 duplication (three copies of the fallback) — consolidation target remains a shared file‑resolver service (10.2). |
|
||||||
| 1.8 | 2026‑08‑13 | **Defect fix and interface change (additive).** Content Blocks never carried the Core *Appearance* tab: `layout`, `frame_class` — including the VITEC frame classes — `space_before_class`, `space_after_class`, `sectionIndex` and `linkToTop` were unreachable for editors on all nine blocks. Added centrally for every `vitec_*` type (7.7); the `appearance` envelope is unchanged, its values were merely always default. Side effect: those six columns now also appear raw inside `data` on top‑level blocks (B‑13), and `appearance.layout` is represented differently on the two envelope paths (B‑14). `intro-paragraph` gained `background_color` (7.7). `vitec_eventlist` gained the layout `regions`, emitting a `regions` array built from the region categories below parent 104; the event payload is specified for the first time (7.14). B‑11 and B‑12 recorded as resolved. |
|
| 1.8 | 2026‑08‑13 | **Defect fix and interface change (additive).** Content Blocks never carried the Core *Appearance* tab: `layout`, `frame_class` — including the VITEC frame classes — `space_before_class`, `space_after_class`, `sectionIndex` and `linkToTop` were unreachable for editors on all nine blocks. Added centrally for every `vitec_*` type (7.7); the `appearance` envelope is unchanged, its values were merely always default. Side effect: those six columns now also appear raw inside `data` on top‑level blocks (B‑13), and `appearance.layout` is represented differently on the two envelope paths (B‑14). `intro-paragraph` gained `background_color` (7.7). `vitec_eventlist` gained the layout `regions`, emitting a `regions` array built from the region categories below parent 104; the event payload is specified for the first time (7.14). B‑11 and B‑12 recorded as resolved. |
|
||||||
|
| 1.9 | 2026‑08‑15 | **Interface change, partly breaking.** The four list plugins were unified: every one of them now emits an object carrying `layout`, the new `showToolbar` flag and its payload array. `vitec_usecaselist` and `vitec_productlist` previously emitted a **bare array** — front ends reading them have to move one level down (7.3, 7.14, 8). New plugin **`vitec_solutionlist`** (`SolutionListJsonRenderer`, key `solutions`), the counterpart 7.13.2 had been asking for since v1.2. `tx_vitec_domain_model_usecase` gained `detail_page`, emitted as the resolved `detailUrl` in the story card shape and therefore also in the `vitec_modelcard` story branch, which had carried no link at all until now. Noted: `vitec_productlist` had a configurable `layout` that was never serialised, and its vocabulary (`0`–`3`) differs from the other lists. |
|
||||||
|
|
||||||
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
|
||||||
@@ -383,10 +384,27 @@ a lean variant of the envelope:
|
|||||||
### 7.3 Payload keys
|
### 7.3 Payload keys
|
||||||
| Kind | Key | Cardinality |
|
| Kind | Key | Cardinality |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| List plugin | plural noun (`products`, `usecases`, `news → items`) | array |
|
| List plugin | plural noun (`products`, `usecases`, `markets`, `solutions`, `news → items`) | **object** (see below) |
|
||||||
| Detail plugin | singular noun (`product`, `usecase`, `market`, `solution`) | object |
|
| Detail plugin | singular noun (`product`, `usecase`, `market`, `solution`) | object |
|
||||||
| Container | `items` | array of `{config, contentElements}` |
|
| Container | `items` | array of `{config, contentElements}` |
|
||||||
|
|
||||||
|
**List envelope (since v1.9).** The four record list plugins — `vitec_productlist`,
|
||||||
|
`vitec_usecaselist`, `vitec_marketlist`, `vitec_solutionlist` — share one shape:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "layout": "grid", // display variant, see the note below
|
||||||
|
"showToolbar": false, // render the toolbar above the list
|
||||||
|
"<plural>": [ … ] } // products | usecases | markets | solutions
|
||||||
|
```
|
||||||
|
|
||||||
|
`vitec_usecaselist` and `vitec_productlist` emitted a bare array before v1.9; a consumer has
|
||||||
|
to read `<key>.<key>` now. `layout` uses `grid | list | carousel | 50-50` everywhere **except**
|
||||||
|
`vitec_productlist`, which keeps its own `0`–`3` ("Default", "Variation 1–3") — unifying it
|
||||||
|
would move existing content onto a different layout and needs a migration, so it was left
|
||||||
|
alone.
|
||||||
|
|
||||||
|
`news_*` is not part of this: it keeps its own `{mode, items|news, settings}` envelope (7.6).
|
||||||
|
|
||||||
### 7.4 Container payload
|
### 7.4 Container payload
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
@@ -761,8 +779,23 @@ in v1.3 on the assumption that lists only need `teaser`, which turned out to be
|
|||||||
practice. Image resolution is delegated to `UsecaseSerializer::image()` per 9.2; this
|
practice. Image resolution is delegated to `UsecaseSerializer::image()` per 9.2; this
|
||||||
renderer duplicates no FAL logic.
|
renderer duplicates no FAL logic.
|
||||||
|
|
||||||
A Solution list counterpart does not exist yet. When it is added it **should** reuse this
|
#### 7.13.3 Solution list payload (`vitec_solutionlist`)
|
||||||
shape under `content.solutions`.
|
Added in v1.9, and deliberately the same shape as 7.13.2 — `SolutionListJsonRenderer` emits
|
||||||
|
under `content.solutions`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "layout": "grid", "showToolbar": false,
|
||||||
|
"solutions": [
|
||||||
|
{ "uid": 7, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…",
|
||||||
|
"description": "<p>…</p>", // RTE HTML, resolved per 9.11
|
||||||
|
"detailUrl": "/solutions/iptv/", // from the record's detail_page; null when unset
|
||||||
|
"image": { "url": "…", "srcset": [ … ] } } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Selection and ordering follow 7.13.2 exactly: nothing selected means all visible solutions
|
||||||
|
alphabetically, a selection means precisely those in the arranged order.
|
||||||
|
`tx_vitec_domain_model_solution` has **no `sorting` column** either, which is why the
|
||||||
|
unselected case falls back to alphabetical rather than to a backend‑defined order.
|
||||||
|
|
||||||
### 7.14 Event list payload (`vitec_eventlist`)
|
### 7.14 Event list payload (`vitec_eventlist`)
|
||||||
`EventlistJsonRenderer` emits, under `content.eventlist`:
|
`EventlistJsonRenderer` emits, under `content.eventlist`:
|
||||||
@@ -834,6 +867,7 @@ remaining present in `events`.
|
|||||||
| `vitec_usecaselist` | `< lib.…WithHeader` | UsecaseListJsonRenderer → **UsecaseSerializer** | `usecases` | list |
|
| `vitec_usecaselist` | `< lib.…WithHeader` | UsecaseListJsonRenderer → **UsecaseSerializer** | `usecases` | list |
|
||||||
| `vitec_usecaseshow` | `< lib.…WithHeader` | UsecaseShowJsonRenderer → **UsecaseSerializer** | `usecase` | detail |
|
| `vitec_usecaseshow` | `< lib.…WithHeader` | UsecaseShowJsonRenderer → **UsecaseSerializer** | `usecase` | detail |
|
||||||
| `vitec_marketlist` | `< lib.…WithHeader` | MarketListJsonRenderer | `markets` | list (global) |
|
| `vitec_marketlist` | `< lib.…WithHeader` | MarketListJsonRenderer | `markets` | list (global) |
|
||||||
|
| `vitec_solutionlist` | `< lib.…WithHeader` | SolutionListJsonRenderer | `solutions` | list (global) |
|
||||||
| `vitec_marketshow` | `< lib.…WithHeader` | MarketShowJsonRenderer | `market` | detail |
|
| `vitec_marketshow` | `< lib.…WithHeader` | MarketShowJsonRenderer | `market` | detail |
|
||||||
| `vitec_solutionshow` | `< lib.…WithHeader` | SolutionShowJsonRenderer | `solution` | detail |
|
| `vitec_solutionshow` | `< lib.…WithHeader` | SolutionShowJsonRenderer | `solution` | detail |
|
||||||
| `vitec_downloadcard` | `< lib.…WithHeader` | DownloadcardJsonRenderer | `downloadcard` | detail |
|
| `vitec_downloadcard` | `< lib.…WithHeader` | DownloadcardJsonRenderer | `downloadcard` | detail |
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||||
|
<!--
|
||||||
|
Headless mode: the JSON for vitec_solutionlist is produced by
|
||||||
|
Evomedien\Vitec\UserFunc\SolutionListJsonRenderer, wired in
|
||||||
|
Configuration/TypoScript via the Vitecset setup. This template only exists so
|
||||||
|
SolutionController::listAction() has one to render if the plugin is ever
|
||||||
|
reached through the regular Extbase path.
|
||||||
|
-->
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="#26358c" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3.5" width="8" height="8" rx="1.5"/>
|
||||||
|
<rect x="13" y="3.5" width="8" height="8" rx="1.5"/>
|
||||||
|
<rect x="3" y="13.5" width="8" height="7" rx="1.5"/>
|
||||||
|
<rect x="13" y="13.5" width="8" height="7" rx="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
@@ -104,6 +104,17 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1750000000] = [
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ExtensionUtility::configurePlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Solutionlist',
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\SolutionController::class => 'list'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\SolutionController::class => 'list'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
ExtensionUtility::configurePlugin(
|
ExtensionUtility::configurePlugin(
|
||||||
'Vitec',
|
'Vitec',
|
||||||
'Solutionshow',
|
'Solutionshow',
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ CREATE TABLE tx_vitec_product_related_mm (
|
|||||||
CREATE TABLE tx_vitec_domain_model_usecase (
|
CREATE TABLE tx_vitec_domain_model_usecase (
|
||||||
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,
|
||||||
subtitle varchar(255) DEFAULT '' NOT NULL,
|
subtitle varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
|
||||||
card_image int(11) unsigned DEFAULT '0' NOT NULL,
|
card_image int(11) unsigned DEFAULT '0' NOT NULL,
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<title>VITEC</title>
|
<title>VITEC</title>
|
||||||
<script type="module" crossorigin src="/_frontend/assets/index-CA5rSLMo.js"></script>
|
<script type="module" crossorigin src="/_frontend/assets/index-ARhFRF4y.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-C7oUhBog.css">
|
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-BUkc9k85.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page"></div>
|
<div id="page"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user