diff --git a/packages/vitec/Classes/Controller/SolutionController.php b/packages/vitec/Classes/Controller/SolutionController.php index 59a4305..cdb0027 100644 --- a/packages/vitec/Classes/Controller/SolutionController.php +++ b/packages/vitec/Classes/Controller/SolutionController.php @@ -45,6 +45,19 @@ class SolutionController extends ActionController * * @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 { $selectedSolutionId = (int)$this->settings['solution']; diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php index e22c7b4..f99a8d9 100755 --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php @@ -8,6 +8,7 @@ use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer; use Evomedien\Vitec\UserFunc\MarketListJsonRenderer; +use Evomedien\Vitec\UserFunc\SolutionListJsonRenderer; use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer; use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; @@ -119,6 +120,7 @@ final class ContainerChildrenProcessor implements DataProcessorInterface 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], 'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'], + 'vitec_solutionlist' => [SolutionListJsonRenderer::class, 'solutions'], 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], diff --git a/packages/vitec/Classes/Domain/Model/Usecase.php b/packages/vitec/Classes/Domain/Model/Usecase.php index 62d08fa..958678e 100644 --- a/packages/vitec/Classes/Domain/Model/Usecase.php +++ b/packages/vitec/Classes/Domain/Model/Usecase.php @@ -19,6 +19,14 @@ class Usecase extends AbstractEntity */ 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 * @@ -110,6 +118,16 @@ class Usecase extends AbstractEntity return $this->slug; } + public function getDetailPage(): int + { + return $this->detailPage; + } + + public function setDetailPage(int $detailPage): void + { + $this->detailPage = $detailPage; + } + /** * @param string $slug */ diff --git a/packages/vitec/Classes/Service/ContentElementResolver.php b/packages/vitec/Classes/Service/ContentElementResolver.php index 81a5e47..e5844b1 100755 --- a/packages/vitec/Classes/Service/ContentElementResolver.php +++ b/packages/vitec/Classes/Service/ContentElementResolver.php @@ -10,6 +10,7 @@ use Evomedien\Vitec\UserFunc\ProductShowJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer; use Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer; use Evomedien\Vitec\UserFunc\MarketListJsonRenderer; +use Evomedien\Vitec\UserFunc\SolutionListJsonRenderer; use Evomedien\Vitec\UserFunc\MarketShowJsonRenderer; use Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer; use Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer; @@ -77,6 +78,7 @@ final class ContentElementResolver 'vitec_usecaseshow' => [UsecaseShowJsonRenderer::class, 'usecase'], 'vitec_marketshow' => [MarketShowJsonRenderer::class, 'market'], 'vitec_marketlist' => [MarketListJsonRenderer::class, 'markets'], + 'vitec_solutionlist' => [SolutionListJsonRenderer::class, 'solutions'], 'vitec_solutionshow' => [SolutionShowJsonRenderer::class, 'solution'], 'vitec_downloadcard' => [DownloadcardJsonRenderer::class, 'downloadcard'], 'vitec_downloadcardcollection' => [DownloadcardcollectionJsonRenderer::class, 'downloadcardcollection'], diff --git a/packages/vitec/Classes/Service/UsecaseSerializer.php b/packages/vitec/Classes/Service/UsecaseSerializer.php index 4af152a..1b669cf 100755 --- a/packages/vitec/Classes/Service/UsecaseSerializer.php +++ b/packages/vitec/Classes/Service/UsecaseSerializer.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Evomedien\Vitec\Service; use Doctrine\DBAL\ParameterType; +use Evomedien\Vitec\Service\LinkResolver; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -42,6 +43,10 @@ final class UsecaseSerializer 'uid' => $uid, 'title' => (string)($u['title'] ?? ''), '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'] ?? ''), 'teaser' => (string)($u['teaser'] ?? ''), 'cardImage' => $this->image($uid, 'card_image', null, true), diff --git a/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php b/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php index 2872087..7a3882a 100644 --- a/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php @@ -18,7 +18,7 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; * UserFunc: render all VITEC markets as JSON (headless). * * Output under content.markets: - * { "layout": "grid|list|carousel", "markets": [ … ] } + * { "layout": "grid|list|carousel|50-50", "markets": [ … ] } * * Selection semantics (FlexForm `settings.markets`): * - nothing selected -> ALL visible markets, alphabetical by title @@ -152,6 +152,7 @@ class MarketListJsonRenderer $response = [ 'layout' => $layout, + 'showToolbar' => (bool)($settings['showtoolbar'] ?? false), 'markets' => $markets, ]; diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php index d4977a5..25512c0 100755 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -152,20 +152,30 @@ 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 + // 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) { - return json_encode([ - 'products' => $productsData, - 'debug' => [ - 'pageId' => (int)($GLOBALS['TSFE']->id ?? 0), - 'categoryUids' => $categoryUids, - 'productCount' => count($productsData), - 'settings' => $settings, - 'fallbackFiles' => $this->collectFallbackFilesFromProducts($productsData), - ], - ]); + $response['debug'] = [ + 'pageId' => (int)($GLOBALS['TSFE']->id ?? 0), + 'categoryUids' => $categoryUids, + 'productCount' => count($productsData), + 'settings' => $settings, + 'fallbackFiles' => $this->collectFallbackFilesFromProducts($productsData), + ]; } - return json_encode($productsData); + return json_encode($response); } catch (\Throwable $e) { return ''; } diff --git a/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php b/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php new file mode 100644 index 0000000..523c2e8 --- /dev/null +++ b/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php @@ -0,0 +1,196 @@ + 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 $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 $r + * @return array + */ + 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), + ]; + } +} diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php index 7bf1705..17dc562 100755 --- a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php @@ -20,6 +20,11 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; * specific tt_content row (reused by ContainerChildrenProcessor for nested * usecase-list plugins). All serialisation is delegated to UsecaseSerializer. * 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 { @@ -109,6 +114,8 @@ class UsecaseListJsonRenderer $flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? ''); $settings = $flexFormData['settings'] ?? []; $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); $rows = $qb @@ -139,25 +146,59 @@ class UsecaseListJsonRenderer $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( static function (array $u) use ($serializer, $detailBase): array { $item = $serializer->serializeListItem($u); - $item['detailUrl'] = ($detailBase !== '' && ($item['slug'] ?? '') !== '') - ? $detailBase . '/' . ltrim((string)$item['slug'], '/') - : null; + // A story with its own detail page wins; only the others + // fall back to the Single PID plus the slug, so nothing + // 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; }, $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) { - return (string)json_encode([ - 'usecases' => $usecases, - 'debug' => ['count' => count($usecases), 'settings' => $settings], - ]); + $response['debug'] = [ + 'count' => count($usecases), + 'selected' => $selectedUids, + 'settings' => $settings, + ]; } - return (string)json_encode($usecases); + return (string)json_encode($response); } catch (\Throwable $e) { return ''; } diff --git a/packages/vitec/Configuration/FlexForms/Marketlist.xml b/packages/vitec/Configuration/FlexForms/Marketlist.xml index a347452..5747820 100644 --- a/packages/vitec/Configuration/FlexForms/Marketlist.xml +++ b/packages/vitec/Configuration/FlexForms/Marketlist.xml @@ -28,11 +28,24 @@ carousel + + + 50-50 + grid + + + Show the toolbar above the list. + + check + 0 + + + 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). diff --git a/packages/vitec/Configuration/FlexForms/Productlist.xml b/packages/vitec/Configuration/FlexForms/Productlist.xml index 4daf0f8..edc5de2 100644 --- a/packages/vitec/Configuration/FlexForms/Productlist.xml +++ b/packages/vitec/Configuration/FlexForms/Productlist.xml @@ -92,6 +92,14 @@ + + + Show the toolbar above the list. + + check + 0 + + diff --git a/packages/vitec/Configuration/FlexForms/Solutionlist.xml b/packages/vitec/Configuration/FlexForms/Solutionlist.xml new file mode 100644 index 0000000..1f41412 --- /dev/null +++ b/packages/vitec/Configuration/FlexForms/Solutionlist.xml @@ -0,0 +1,71 @@ + + + + + + Solution List Settings + array + + + + Display variant for the solution cards. + + select + selectSingle + + + + grid + + + + list + + + + carousel + + + + 50-50 + + + grid + + + + + + Show the toolbar above the list. + + check + 0 + + + + + + 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). + + select + selectMultipleSideBySide + tx_vitec_domain_model_solution + AND tx_vitec_domain_model_solution.hidden = 0 AND tx_vitec_domain_model_solution.deleted = 0 ORDER BY tx_vitec_domain_model_solution.title + 8 + 0 + 999 + + + + + + + check + 0 + + + + + + + diff --git a/packages/vitec/Configuration/FlexForms/Usecaselist.xml b/packages/vitec/Configuration/FlexForms/Usecaselist.xml index 3b70721..cf4dc0b 100644 --- a/packages/vitec/Configuration/FlexForms/Usecaselist.xml +++ b/packages/vitec/Configuration/FlexForms/Usecaselist.xml @@ -6,6 +6,57 @@ Usecase List array + + + Display variant for the success story cards. + + select + selectSingle + + + + grid + + + + list + + + + carousel + + + + 50-50 + + + grid + + + + + + Show the toolbar above the list. + + check + 0 + + + + + + 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). + + select + selectMultipleSideBySide + tx_vitec_domain_model_usecase + 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 + 8 + 0 + 999 + + + Page that holds the "Show single VITEC Success Story" plugin. Used to build the detail links in the JSON output. diff --git a/packages/vitec/Configuration/Icons.php b/packages/vitec/Configuration/Icons.php index 729c1bb..c5e4f18 100755 --- a/packages/vitec/Configuration/Icons.php +++ b/packages/vitec/Configuration/Icons.php @@ -84,6 +84,10 @@ return [ 'provider' => SvgIconProvider::class, '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' => [ 'provider' => SvgIconProvider::class, 'source' => 'EXT:vitec/Resources/Public/Icons/vitec-import.svg', diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript index d8fa192..0b52957 100755 --- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript +++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript @@ -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 { fields { diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content.php b/packages/vitec/Configuration/TCA/Overrides/tt_content.php index 18b97f9..d79b06d 100644 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content.php @@ -111,6 +111,16 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility; '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. $GLOBALS['TCA']['tt_content']['columns']['frame_class']['config']['renderType'] = 'selectCheckBox'; diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php index 8a16493..72c6724 100644 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php +++ b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_usecase.php @@ -26,7 +26,7 @@ return [ '1' => [ 'showitem' => '--div--;General, - title, slug, subtitle, + title, slug, detail_page, subtitle, --div--;List view, card_image, customer_logo, teaser, --div--;Detail · Hero, @@ -109,6 +109,17 @@ return [ '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' => [ 'label' => 'Subtitle', 'config' => ['type' => 'input', 'size' => 40, 'eval' => 'trim', 'default' => ''], diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md index 25b4fdf..eb7c392 100755 --- a/packages/vitec/Documentation/Headless-JSON-Architecture.md +++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md @@ -3,9 +3,9 @@ | | | |---|---| | **Document identifier** | EVO‑VITEC‑HL‑001 | -| **Version** | 1.8 | +| **Version** | 1.9 | | **Status** | Released | -| **Date** | 2026‑08‑13 | +| **Date** | 2026‑08‑15 | | **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) | | **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.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.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, 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 | 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 | | 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 + "": [ … ] } // products | usecases | markets | solutions +``` + +`vitec_usecaselist` and `vitec_productlist` emitted a bare array before v1.9; a consumer has +to read `.` 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 ```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 renderer duplicates no FAL logic. -A Solution list counterpart does not exist yet. When it is added it **should** reuse this -shape under `content.solutions`. +#### 7.13.3 Solution list payload (`vitec_solutionlist`) +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": "

", // 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`) `EventlistJsonRenderer` emits, under `content.eventlist`: @@ -834,6 +867,7 @@ remaining present in `events`. | `vitec_usecaselist` | `< lib.…WithHeader` | UsecaseListJsonRenderer → **UsecaseSerializer** | `usecases` | list | | `vitec_usecaseshow` | `< lib.…WithHeader` | UsecaseShowJsonRenderer → **UsecaseSerializer** | `usecase` | detail | | `vitec_marketlist` | `< lib.…WithHeader` | MarketListJsonRenderer | `markets` | list (global) | +| `vitec_solutionlist` | `< lib.…WithHeader` | SolutionListJsonRenderer | `solutions` | list (global) | | `vitec_marketshow` | `< lib.…WithHeader` | MarketShowJsonRenderer | `market` | detail | | `vitec_solutionshow` | `< lib.…WithHeader` | SolutionShowJsonRenderer | `solution` | detail | | `vitec_downloadcard` | `< lib.…WithHeader` | DownloadcardJsonRenderer | `downloadcard` | detail | diff --git a/packages/vitec/Resources/Private/Templates/Solution/List.html b/packages/vitec/Resources/Private/Templates/Solution/List.html new file mode 100644 index 0000000..6871257 --- /dev/null +++ b/packages/vitec/Resources/Private/Templates/Solution/List.html @@ -0,0 +1,9 @@ + + + diff --git a/packages/vitec/Resources/Public/Icons/vitec-plugin-solutionlist.svg b/packages/vitec/Resources/Public/Icons/vitec-plugin-solutionlist.svg new file mode 100644 index 0000000..a83784b --- /dev/null +++ b/packages/vitec/Resources/Public/Icons/vitec-plugin-solutionlist.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/packages/vitec/ext_localconf.php b/packages/vitec/ext_localconf.php index 87714d0..d2d539f 100755 --- a/packages/vitec/ext_localconf.php +++ b/packages/vitec/ext_localconf.php @@ -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( 'Vitec', 'Solutionshow', diff --git a/packages/vitec/ext_tables.sql b/packages/vitec/ext_tables.sql index 10b876a..e4ec048 100755 --- a/packages/vitec/ext_tables.sql +++ b/packages/vitec/ext_tables.sql @@ -95,6 +95,7 @@ CREATE TABLE tx_vitec_product_related_mm ( CREATE TABLE tx_vitec_domain_model_usecase ( title 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, card_image int(11) unsigned DEFAULT '0' NOT NULL, diff --git a/public/_frontend/index.html b/public/_frontend/index.html index 51a66a8..c561421 100644 --- a/public/_frontend/index.html +++ b/public/_frontend/index.html @@ -25,8 +25,8 @@ VITEC - - + +