diff --git a/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php b/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php index 294685d..9e3414e 100644 --- a/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php +++ b/packages/vitec/Classes/Controller/Backend/ProductTextImportController.php @@ -95,6 +95,13 @@ final class ProductTextImportController 'description' => 'c:Product Introduction#1:body', 'description2' => 'c:Product Overview — H2#1:body', 'capabilities' => 'g:Key Capability Group', + // Decisions 2026-09-18 (field walkthrough completed): the new + // 'Technical Highlight' spec rows and the Why-Choose benefit cards + // each compose into one block; meta description is trimmed from the + // intro because the SEO sheet still ships none. + 'highlights' => 'g:Technical Highlight', + 'applications' => 'g:Why Choose Card', + 'seometa' => 'x:seometa', 'textrelatedproducts' => 'p:Related Products — H2#1', 'keywords' => 'm:primaryKeyword', ]; @@ -372,7 +379,12 @@ final class ProductTextImportController } if (!empty($body['saveMapping'])) { - $this->mappingRepository->save(self::MAPPING_KEY, array_filter($map), '-'); + // Merge instead of overwrite: a field whose dropdown was empty on + // THIS submit keeps its stored selector (this is how `teaser` got + // lost once). Clearing a mapping on purpose means picking another + // source, not leaving the select blank. + $stored = $this->mappingRepository->load(self::MAPPING_KEY)['mapping'] ?? []; + $this->mappingRepository->save(self::MAPPING_KEY, array_filter($map) + $stored, '-'); } $summary = []; diff --git a/packages/vitec/Classes/Domain/Model/Product.php b/packages/vitec/Classes/Domain/Model/Product.php index fdb25da..4e18866 100755 --- a/packages/vitec/Classes/Domain/Model/Product.php +++ b/packages/vitec/Classes/Domain/Model/Product.php @@ -722,6 +722,7 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $this->productimage = new ObjectStorage(); $this->heroimage = new ObjectStorage(); $this->downloads = new ObjectStorage(); + $this->solutions = new ObjectStorage(); } /** @@ -1070,6 +1071,32 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $this->textrelatedproducts = $textrelatedproducts; } + /** + * Solutions this product belongs to (MM). No companion property for the + * `resources` child table on purpose: it has no Extbase model - the + * headless renderers read tx_vitec_product_resource directly and the + * Extbase path never touches it. + * + * @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Solution> + */ + protected $solutions; + + /** + * @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Solution> + */ + public function getSolutions(): ObjectStorage + { + return $this->solutions; + } + + /** + * @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Solution> $solutions + */ + public function setSolutions(ObjectStorage $solutions): void + { + $this->solutions = $solutions; + } + /* --------------------------------------------------------------------- */ } diff --git a/packages/vitec/Classes/Import/ProductXlsxReader.php b/packages/vitec/Classes/Import/ProductXlsxReader.php index de6c86a..99ed913 100644 --- a/packages/vitec/Classes/Import/ProductXlsxReader.php +++ b/packages/vitec/Classes/Import/ProductXlsxReader.php @@ -227,6 +227,10 @@ final class ProductXlsxReader if ($seoTitle !== null) { $groups['Composed'][] = $this->entry('x:seotitle', 'Hero title — Intro heading (SEO title)', $seoTitle); } + $seoMeta = $this->composeSeoMeta($parsed); + if ($seoMeta !== null) { + $groups['Composed'][] = $this->entry('x:seometa', 'Introduction body, trimmed to ~155 chars (meta description)', $seoMeta); + } foreach ($parsed['seo'] ?? [] as $header => $value) { $groups['Page SEO Check'][] = $this->entry('s:' . $header, 'SEO · ' . $header, (string)$value); @@ -262,6 +266,9 @@ final class ProductXlsxReader if ($selector === 'x:seotitle') { return $this->composeSeoTitle($parsed); } + if ($selector === 'x:seometa') { + return $this->composeSeoMeta($parsed); + } if (str_starts_with($selector, 's:')) { $value = $parsed['seo'][substr($selector, 2)] ?? null; return is_string($value) && $value !== '' ? $value : null; @@ -310,6 +317,37 @@ final class ProductXlsxReader return $hero . ' — ' . $intro; } + /** + * Meta description from the Product Introduction body: plain text, + * whitespace collapsed, cut at a word boundary to the customary ~155 + * characters with an ellipsis. Decision 2026-09-18: composed here because + * the SEO sheet still ships no meta description at all - a trimmed intro + * beats an empty tag at go-live, and the editor can always overwrite. + */ + private function composeSeoMeta(array $parsed): ?string + { + foreach ($parsed['components'] ?? [] as $component) { + if ((string)($component['component'] ?? '') === 'Product Introduction' + && (int)($component['occurrence'] ?? 1) === 1 + ) { + $text = trim((string)preg_replace('/\s+/u', ' ', strip_tags((string)($component['body'] ?? '')))); + if ($text === '') { + return null; + } + if (mb_strlen($text) <= 155) { + return $text; + } + $cut = mb_substr($text, 0, 152); + $lastSpace = mb_strrpos($cut, ' '); + if ($lastSpace !== false && $lastSpace > 100) { + $cut = mb_substr($cut, 0, $lastSpace); + } + return rtrim($cut, " ,;:") . ' …'; + } + } + return null; + } + /** * One component row as a heading/paragraph pair. The markup - centred h3 * with an inner span, centred p - is copied verbatim from what the diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php index 0f20425..24238a7 100755 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -488,6 +488,7 @@ class ProductListJsonRenderer 'ogimage' => $this->getProductOgImage($uid), 'videofile' => $this->getProductVideoFile($uid), 'relatedprodukt' => $this->getRelatedProducts($uid), + 'solutions' => $this->getProductSolutions($uid), ]; } @@ -911,6 +912,48 @@ class ProductListJsonRenderer return $fallbackFiles; } + /** + * Solutions linked to this product (tx_vitec_product_solution_mm, editor + * order) in the shared solution card shape - the same fields the solution + * list emits, so the front end reuses its card component. detailUrl is + * null while a solution has no detail page (9.12 contract). + */ + protected function getProductSolutions(int $productUid): array + { + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_solution'); + $rows = $queryBuilder + ->select('s.*') + ->from('tx_vitec_domain_model_solution', 's') + ->join('s', 'tx_vitec_product_solution_mm', 'mm', 'mm.uid_foreign = s.uid') + ->where( + $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), + $queryBuilder->expr()->eq('s.deleted', 0), + $queryBuilder->expr()->eq('s.hidden', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + $serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class); + $result = []; + foreach ($rows as $solutionRow) { + $solutionUid = (int)$solutionRow['uid']; + $result[] = [ + 'uid' => $solutionUid, + 'title' => (string)($solutionRow['title'] ?? ''), + 'slug' => (string)($solutionRow['slug'] ?? ''), + 'subtitle' => (string)($solutionRow['subtitle'] ?? ''), + 'teaser' => (string)($solutionRow['teaser'] ?? ''), + 'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''), + 'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)), + 'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true), + ]; + } + + return $result; + } + protected function getProductDownloads(int $productUid): array { $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php index d56687b..d99a3f4 100755 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php @@ -252,6 +252,10 @@ class ProductShowJsonRenderer 'ogimage' => $this->getProductOgImage($uid), 'videofile' => $this->getProductVideoFile($uid), 'relatedprodukt' => $this->getRelatedProducts($uid), + 'solutions' => $this->getProductSolutions($uid), + // Detail-only like heroimage: resource cards are detail-page + // furniture a list never shows. + 'resources' => $this->getProductResources($uid), ]; // schema.org JSON-LD (Product + optional VideoObject) as a ready-to-emit @@ -732,6 +736,93 @@ class ProductShowJsonRenderer return $fallbackFiles; } + /** + * Solutions linked to this product (tx_vitec_product_solution_mm, editor + * order) in the shared solution card shape - the same fields the solution + * list emits, so the front end reuses its card component. detailUrl is + * null while a solution has no detail page (9.12 contract). + */ + protected function getProductSolutions(int $productUid): array + { + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_solution'); + $rows = $queryBuilder + ->select('s.*') + ->from('tx_vitec_domain_model_solution', 's') + ->join('s', 'tx_vitec_product_solution_mm', 'mm', 'mm.uid_foreign = s.uid') + ->where( + $queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), + $queryBuilder->expr()->eq('s.deleted', 0), + $queryBuilder->expr()->eq('s.hidden', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + $serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class); + $result = []; + foreach ($rows as $solutionRow) { + $solutionUid = (int)$solutionRow['uid']; + $result[] = [ + 'uid' => $solutionUid, + 'title' => (string)($solutionRow['title'] ?? ''), + 'slug' => (string)($solutionRow['slug'] ?? ''), + 'subtitle' => (string)($solutionRow['subtitle'] ?? ''), + 'teaser' => (string)($solutionRow['teaser'] ?? ''), + 'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''), + 'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)), + 'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true), + ]; + } + + return $result; + } + + /** + * Resource cards from the product's Resources tab (child table + * tx_vitec_product_resource): documentation / support / datasheet + * entries, each an editable card (title, text, icon) around one link - + * page, external URL, file or download record, resolved per 9.12. A card + * whose link is empty or unresolvable is dropped (decision 2026-09-24). + * List envelope: `layout` switches to `carousel` above three cards. + */ + protected function getProductResources(int $productUid): array + { + $queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_product_resource'); + $rows = $queryBuilder + ->select('*') + ->from('tx_vitec_product_resource') + ->where( + $queryBuilder->expr()->eq('product', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), + $queryBuilder->expr()->eq('deleted', 0), + $queryBuilder->expr()->eq('hidden', 0) + ) + ->orderBy('sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + $items = []; + foreach ($rows as $resourceRow) { + $url = \Evomedien\Vitec\Service\LinkResolver::typolinkUrl((string)($resourceRow['link'] ?? '')); + if ($url === null) { + continue; + } + $items[] = [ + 'type' => (string)($resourceRow['restype'] ?? ''), + 'title' => (string)($resourceRow['title'] ?? ''), + 'text' => (string)($resourceRow['cardtext'] ?? ''), + 'icon' => (string)($resourceRow['icon'] ?? ''), + 'url' => $url, + ]; + } + + return [ + 'layout' => count($items) > 3 ? 'carousel' : 'grid', + 'items' => $items, + ]; + } + protected function getProductDownloads(int $productUid): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php index e5f5fac..c9eb9d1 100755 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php +++ b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php @@ -23,7 +23,8 @@ return [ ], ], 'types' => [ - '1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt, + '1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt, solutions, + --div--;Resources, resources, --div--;SEO, seotitle, urltitle, seometa, keywords, structureddata, --div--;Images and Videos, heroimage, productimage, ogimage, video, videofile, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, @@ -431,6 +432,37 @@ return [ 'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering ], ], + 'solutions' => [ + 'exclude' => true, + 'label' => 'Related Solutions', + 'description' => 'Solutions this product belongs to - emitted as solution cards', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_vitec_domain_model_solution', + 'MM' => 'tx_vitec_product_solution_mm', + 'size' => 10, + 'maxitems' => 9999, + 'minitems' => 0, + 'enableMultiSelectFilterTextfield' => true, + ], + ], + 'resources' => [ + 'exclude' => true, + 'label' => 'Resources', + 'description' => 'Resource cards (documentation, support, datasheet) on the product detail page. A card without a link is not emitted.', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_vitec_product_resource', + 'foreign_field' => 'product', + 'foreign_sortby' => 'sorting', + 'appearance' => [ + 'collapseAll' => true, + 'levelLinksPosition' => 'top', + 'useSortable' => true, + ], + ], + ], 'productimage' => [ 'exclude' => true, 'label' => 'Product Image(s)', diff --git a/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php b/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php new file mode 100644 index 0000000..d9cc146 --- /dev/null +++ b/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php @@ -0,0 +1,116 @@ + [ + 'title' => 'Product Resource', + 'label' => 'title', + 'label_alt' => 'restype', + 'label_alt_force' => true, + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'delete' => 'deleted', + 'sortby' => 'sorting', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + ], + 'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_download.gif', + 'security' => [ + 'ignorePageTypeRestriction' => true, + ], + ], + 'types' => [ + '1' => ['showitem' => 'restype, title, cardtext, icon, link, hidden'], + ], + 'columns' => [ + 'hidden' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'items' => [ + [ + 'label' => '', + 'invertStateDisplay' => true, + ], + ], + ], + ], + 'product' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'restype' => [ + 'exclude' => true, + 'label' => 'Type', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => 'Product Documentation', 'value' => 'documentation'], + ['label' => 'Technical Support', 'value' => 'support'], + ['label' => 'Datasheet', 'value' => 'datasheet'], + ], + 'default' => 'documentation', + ], + ], + 'title' => [ + 'exclude' => true, + 'label' => 'Card title', + 'config' => [ + 'type' => 'input', + 'size' => 40, + 'eval' => 'trim', + 'default' => '', + ], + ], + 'cardtext' => [ + 'exclude' => true, + 'label' => 'Card text', + 'config' => [ + 'type' => 'text', + 'cols' => 40, + 'rows' => 3, + 'eval' => 'trim', + ], + ], + 'icon' => [ + 'exclude' => true, + 'label' => 'Icon', + 'description' => 'Icon identifier as used by the front end. Free text; the picker offers the seed set.', + 'config' => [ + 'type' => 'input', + 'size' => 20, + 'eval' => 'trim', + 'default' => '', + 'valuePicker' => [ + 'items' => [ + ['label' => 'Document', 'value' => 'document'], + ['label' => 'Support', 'value' => 'support'], + ['label' => 'Datasheet', 'value' => 'datasheet'], + ], + ], + ], + ], + 'link' => [ + 'exclude' => true, + 'label' => 'Link', + 'description' => 'Page, external URL, file or a Download record (link browser tab "Download"). A card without a link is not emitted to the front end.', + 'config' => [ + 'type' => 'link', + 'allowedTypes' => ['page', 'url', 'file', 'record'], + 'size' => 50, + ], + ], + ], +]; diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md index c85e2ef..80c087c 100755 --- a/packages/vitec/Documentation/Headless-JSON-Architecture.md +++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md @@ -30,6 +30,7 @@ | 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. | | 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. | | 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. | +| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support / datasheet cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted), delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards (7.15). Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). | 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 @@ -897,12 +898,14 @@ Produced by `ProductShowJsonRenderer::serializeProduct()`. Members: | `downloads` | array | download records incl. the resolved `file` | | `ogimage`, `videofile` | object \| null | FAL singletons | | `relatedprodukt` | array | card shape `{uid, title, slug, subtitle, teaser, description, link, images}`, backend order | +| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, image}` via `tx_vitec_product_solution_mm`, backend order | +| `resources` | object | list envelope `{layout, items[]}` of hand‑written resource cards: `type` (`documentation\|support\|datasheet`), `title`, `text`, `icon`, resolved `url` (9.12); a card without a resolvable link is dropped; `layout` is `carousel` above three cards — **detail payload only** | | `jsonLd` | string | ready‑to‑emit schema.org `@graph` (6.6), detail only | #### 7.15.2 List payload (`vitec_productlist`, key `products`) List envelope per 7.3 (`layout`, `showToolbar`, `products[]`) — since v1.9. Each item -carries the member set of 7.15.1 **except** `heroimage` and `jsonLd`. `heroimage` is +carries the member set of 7.15.1 **except** `heroimage`, `resources` and `jsonLd`. `heroimage` is deliberately detail‑only: one further FAL query per product would buy data a list rarely shows. The `layout` vocabulary remains `0`–`3` (see the v1.9 note). @@ -1469,4 +1472,4 @@ remediation. - Header convention — the uniform header section across CEs, plugins and containers. - `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point. -*End of document EVO‑VITEC‑HL‑001 v1.15.* +*End of document EVO‑VITEC‑HL‑001 v1.18.* diff --git a/packages/vitec/ext_tables.sql b/packages/vitec/ext_tables.sql index 10674e5..1cfcf8b 100755 --- a/packages/vitec/ext_tables.sql +++ b/packages/vitec/ext_tables.sql @@ -58,6 +58,8 @@ CREATE TABLE tx_vitec_domain_model_product ( capabilities text, portfolio varchar(1024) DEFAULT '' NOT NULL, textrelatedproducts text, + solutions int(11) DEFAULT '0' NOT NULL, + resources int(11) DEFAULT '0' NOT NULL, KEY parent (pid) ); CREATE TABLE tx_vitec_domain_model_download ( @@ -97,6 +99,32 @@ CREATE TABLE tx_vitec_product_related_mm ( PRIMARY KEY (uid_local, uid_foreign) ); +CREATE TABLE tx_vitec_product_solution_mm ( + uid_local INT(11) NOT NULL, + uid_foreign INT(11) NOT NULL, + sorting INT(11) DEFAULT '0' NOT NULL, + PRIMARY KEY (uid_local, uid_foreign) +); + +CREATE TABLE tx_vitec_product_resource ( + uid int(11) NOT NULL auto_increment, + pid int(11) DEFAULT '0' NOT NULL, + tstamp int(11) DEFAULT '0' NOT NULL, + crdate int(11) DEFAULT '0' NOT NULL, + deleted tinyint(4) DEFAULT '0' NOT NULL, + hidden tinyint(4) DEFAULT '0' NOT NULL, + sorting int(11) DEFAULT '0' NOT NULL, + product int(11) unsigned DEFAULT '0' NOT NULL, + restype varchar(32) DEFAULT 'documentation' NOT NULL, + title varchar(255) DEFAULT '' NOT NULL, + cardtext text, + icon varchar(64) DEFAULT '' NOT NULL, + link varchar(1024) DEFAULT '' NOT NULL, + PRIMARY KEY (uid), + KEY parent (pid), + KEY product (product) +); + CREATE TABLE tx_vitec_domain_model_usecase ( title varchar(255) DEFAULT '' NOT NULL, slug varchar(255) DEFAULT '' NOT NULL,