Compare commits
2 Commits
d846ba2007
...
7833e1b668
| Author | SHA1 | Date | |
|---|---|---|---|
| 7833e1b668 | |||
| 7442d10e68 |
330
composer.lock
generated
330
composer.lock
generated
@@ -338,16 +338,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
"version": "1.5.13",
|
||||
"version": "1.5.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/ca-bundle.git",
|
||||
"reference": "c008272789979f709f7fcb32c2ecf1d2db5e84e5"
|
||||
"reference": "0c8abba0634f637bd78c4e451981da368d403463"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/c008272789979f709f7fcb32c2ecf1d2db5e84e5",
|
||||
"reference": "c008272789979f709f7fcb32c2ecf1d2db5e84e5",
|
||||
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c8abba0634f637bd78c4e451981da368d403463",
|
||||
"reference": "0c8abba0634f637bd78c4e451981da368d403463",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -394,7 +394,7 @@
|
||||
"support": {
|
||||
"irc": "irc://irc.freenode.org/composer",
|
||||
"issues": "https://github.com/composer/ca-bundle/issues",
|
||||
"source": "https://github.com/composer/ca-bundle/tree/1.5.13"
|
||||
"source": "https://github.com/composer/ca-bundle/tree/1.5.14"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -406,7 +406,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-18T12:35:13+00:00"
|
||||
"time": "2026-08-21T14:57:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/class-map-generator",
|
||||
@@ -479,16 +479,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/composer",
|
||||
"version": "2.10.2",
|
||||
"version": "2.10.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/composer.git",
|
||||
"reference": "8d4439f572a97670a9edc039eb3b093cc976b4bc"
|
||||
"reference": "f0de0bf90226853b841672f086d8b58b02332504"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/composer/zipball/8d4439f572a97670a9edc039eb3b093cc976b4bc",
|
||||
"reference": "8d4439f572a97670a9edc039eb3b093cc976b4bc",
|
||||
"url": "https://api.github.com/repos/composer/composer/zipball/f0de0bf90226853b841672f086d8b58b02332504",
|
||||
"reference": "f0de0bf90226853b841672f086d8b58b02332504",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -499,6 +499,8 @@
|
||||
"composer/semver": "^3.3",
|
||||
"composer/spdx-licenses": "^1.5.7",
|
||||
"composer/xdebug-handler": "^2.0.2 || ^3.0.3",
|
||||
"ext-filter": "*",
|
||||
"ext-hash": "*",
|
||||
"ext-json": "*",
|
||||
"justinrainbow/json-schema": "^6.5.1",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
@@ -576,7 +578,7 @@
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/composer/issues",
|
||||
"security": "https://github.com/composer/composer/security/policy",
|
||||
"source": "https://github.com/composer/composer/tree/2.10.2"
|
||||
"source": "https://github.com/composer/composer/tree/2.10.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -588,7 +590,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-01T09:24:45+00:00"
|
||||
"time": "2026-08-27T11:34:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/metadata-minifier",
|
||||
@@ -1801,22 +1803,22 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.15.3",
|
||||
"version": "7.15.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc"
|
||||
"reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
|
||||
"reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a",
|
||||
"reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^2.5.2",
|
||||
"guzzlehttp/psr7": "^2.13",
|
||||
"guzzlehttp/promises": "^2.5.3",
|
||||
"guzzlehttp/psr7": "^2.13.1",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
@@ -1909,7 +1911,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/guzzle/issues",
|
||||
"source": "https://github.com/guzzle/guzzle/tree/7.15.3"
|
||||
"source": "https://github.com/guzzle/guzzle/tree/7.15.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1925,20 +1927,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-05T19:48:21+00:00"
|
||||
"time": "2026-08-24T09:21:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/promises.git",
|
||||
"reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce"
|
||||
"reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
|
||||
"reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1",
|
||||
"reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1993,7 +1995,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/promises/issues",
|
||||
"source": "https://github.com/guzzle/promises/tree/2.5.2"
|
||||
"source": "https://github.com/guzzle/promises/tree/2.5.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2009,20 +2011,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-05T19:30:54+00:00"
|
||||
"time": "2026-08-24T09:11:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "2.13.0",
|
||||
"version": "2.13.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4"
|
||||
"reference": "95e7828100de18b4e269fb1703be530082d5166d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4",
|
||||
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d",
|
||||
"reference": "95e7828100de18b4e269fb1703be530082d5166d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2112,7 +2114,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/psr7/issues",
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.13.0"
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.13.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2128,7 +2130,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-16T22:23:49+00:00"
|
||||
"time": "2026-08-24T09:13:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "halaxa/json-machine",
|
||||
@@ -2194,19 +2196,20 @@
|
||||
},
|
||||
{
|
||||
"name": "justinrainbow/json-schema",
|
||||
"version": "6.10.0",
|
||||
"version": "6.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jsonrainbow/json-schema.git",
|
||||
"reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33"
|
||||
"reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33",
|
||||
"reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33",
|
||||
"url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/7e420a943a6fbc95e60e3cf67acfbee85b3b4da7",
|
||||
"reference": "7e420a943a6fbc95e60e3cf67acfbee85b3b4da7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-filter": "*",
|
||||
"ext-json": "*",
|
||||
"marc-mabe/php-enum": "^4.4",
|
||||
"php": "^7.2 || ^8.0"
|
||||
@@ -2239,20 +2242,9 @@
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bruno Prieto Reis",
|
||||
"email": "bruno.p.reis@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Justin Rainbow",
|
||||
"email": "justin.rainbow@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Igor Wiedler",
|
||||
"email": "igor@wiedler.ch"
|
||||
},
|
||||
{
|
||||
"name": "Robert Schönthal",
|
||||
"email": "seroscho@googlemail.com"
|
||||
"name": "Danny van der Sluijs",
|
||||
"email": "danny.vandersluijs@icloud.com",
|
||||
"role": "Maintainer"
|
||||
}
|
||||
],
|
||||
"description": "A library to validate a json schema.",
|
||||
@@ -2263,9 +2255,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/jsonrainbow/json-schema/issues",
|
||||
"source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0"
|
||||
"source": "https://github.com/jsonrainbow/json-schema/tree/6.11.0"
|
||||
},
|
||||
"time": "2026-06-16T20:50:26+00:00"
|
||||
"time": "2026-08-21T10:30:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lolli42/finediff",
|
||||
@@ -2592,24 +2584,24 @@
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.1",
|
||||
"version": "2.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fd5018f6815fff903946d0564977b44ce8010e29"
|
||||
"reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29",
|
||||
"reference": "fd5018f6815fff903946d0564977b44ce8010e29",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7",
|
||||
"reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10"
|
||||
"phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -2653,9 +2645,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.1"
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.11.0"
|
||||
},
|
||||
"time": "2026-06-23T18:43:15+00:00"
|
||||
"time": "2026-08-18T06:18:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "netzbewegung/nb-headless-content-blocks",
|
||||
@@ -4563,16 +4555,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/config",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/config.git",
|
||||
"reference": "e896f5e874e6983d0a098f554ca82a08a924c298"
|
||||
"reference": "696e12da8eea497a1a3808d714b43ff67a7df43e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/e896f5e874e6983d0a098f554ca82a08a924c298",
|
||||
"reference": "e896f5e874e6983d0a098f554ca82a08a924c298",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/696e12da8eea497a1a3808d714b43ff67a7df43e",
|
||||
"reference": "696e12da8eea497a1a3808d714b43ff67a7df43e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4618,7 +4610,7 @@
|
||||
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/config/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/config/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4638,20 +4630,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-30T15:04:16+00:00"
|
||||
"time": "2026-08-20T09:55:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d"
|
||||
"reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/f4c69c9aed03abf933b294257d618bdd9b30a06d",
|
||||
"reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/962e18f09ebe68a49039b4c82fc0ea4871824fca",
|
||||
"reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4716,7 +4708,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4736,20 +4728,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-31T12:37:14+00:00"
|
||||
"time": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dependency-injection",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dependency-injection.git",
|
||||
"reference": "7f59a843c1fbcceafecdf6863d3e38ea6ecd5061"
|
||||
"reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/7f59a843c1fbcceafecdf6863d3e38ea6ecd5061",
|
||||
"reference": "7f59a843c1fbcceafecdf6863d3e38ea6ecd5061",
|
||||
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f318ac9da5aba0be2cf43ecdd562c05a33bc11c0",
|
||||
"reference": "f318ac9da5aba0be2cf43ecdd562c05a33bc11c0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4800,7 +4792,7 @@
|
||||
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4820,7 +4812,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-06T09:45:22+00:00"
|
||||
"time": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -5204,16 +5196,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.15",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79"
|
||||
"reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ff16a16bf87fdf264638b8f6995b3515975e3c79",
|
||||
"reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ee7bc7bca4c7079b88e57d5000aeeb20df570c8d",
|
||||
"reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5250,7 +5242,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.15"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5270,20 +5262,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-22T07:36:05+00:00"
|
||||
"time": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.4.14",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "13b38720174286f55d1761152b575a8d1436fc25"
|
||||
"reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25",
|
||||
"reference": "13b38720174286f55d1761152b575a8d1436fc25",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6",
|
||||
"reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5318,7 +5310,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v7.4.14"
|
||||
"source": "https://github.com/symfony/finder/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5338,20 +5330,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-27T08:31:18+00:00"
|
||||
"time": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "b676451bb638e99a7d34d8a2be90406822e301eb"
|
||||
"reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/b676451bb638e99a7d34d8a2be90406822e301eb",
|
||||
"reference": "b676451bb638e99a7d34d8a2be90406822e301eb",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/2ebe78c083501dfb9509b31a7aedcae4d60a391f",
|
||||
"reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5400,7 +5392,7 @@
|
||||
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5420,20 +5412,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-07T11:50:27+00:00"
|
||||
"time": "2026-08-20T09:55:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mailer",
|
||||
"version": "v7.4.15",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mailer.git",
|
||||
"reference": "68c1f27c97edd0222eb8d440a6c8c4da5354ab46"
|
||||
"reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/68c1f27c97edd0222eb8d440a6c8c4da5354ab46",
|
||||
"reference": "68c1f27c97edd0222eb8d440a6c8c4da5354ab46",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/b17c9bf3a551d5f635638a3b6c05f06c4dc87584",
|
||||
"reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5484,7 +5476,7 @@
|
||||
"description": "Helps sending emails",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mailer/tree/v7.4.15"
|
||||
"source": "https://github.com/symfony/mailer/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5504,20 +5496,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-28T07:33:02+00:00"
|
||||
"time": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/messenger",
|
||||
"version": "v7.4.15",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/messenger.git",
|
||||
"reference": "6338eed8f65c593469fdbdbe043d2a7552264403"
|
||||
"reference": "3976018381c403e369d3027592e911b979eaeed9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/messenger/zipball/6338eed8f65c593469fdbdbe043d2a7552264403",
|
||||
"reference": "6338eed8f65c593469fdbdbe043d2a7552264403",
|
||||
"url": "https://api.github.com/repos/symfony/messenger/zipball/3976018381c403e369d3027592e911b979eaeed9",
|
||||
"reference": "3976018381c403e369d3027592e911b979eaeed9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5578,7 +5570,7 @@
|
||||
"description": "Helps applications send and receive messages to/from other applications or via message queues",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/messenger/tree/v7.4.15"
|
||||
"source": "https://github.com/symfony/messenger/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5598,20 +5590,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-22T12:46:20+00:00"
|
||||
"time": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5"
|
||||
"reference": "bf328d82105831db3e409195db0540ff57f27c80"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/20094b76a7106dbe978d31cd3bd7aa1ed248d0e5",
|
||||
"reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/bf328d82105831db3e409195db0540ff57f27c80",
|
||||
"reference": "bf328d82105831db3e409195db0540ff57f27c80",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5635,7 +5627,7 @@
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||
"symfony/property-info": "^6.4|^7.0|^8.0",
|
||||
"symfony/serializer": "^6.4.3|^7.0.3|^8.0"
|
||||
"symfony/serializer": "^6.4.44|^7.4.17|^8.1.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -5667,7 +5659,7 @@
|
||||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/mime/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5687,7 +5679,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-07T14:56:57+00:00"
|
||||
"time": "2026-08-22T09:04:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
@@ -5927,16 +5919,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.38.1",
|
||||
"version": "v1.42.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
"reference": "dc21118016c039a66235cf93d96b435ffb282412"
|
||||
"reference": "51b5ff5ba85452b31ec6f55490b08148612339d9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412",
|
||||
"reference": "dc21118016c039a66235cf93d96b435ffb282412",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/51b5ff5ba85452b31ec6f55490b08148612339d9",
|
||||
"reference": "51b5ff5ba85452b31ec6f55490b08148612339d9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5990,7 +5982,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1"
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.42.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6010,20 +6002,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-25T15:22:23+00:00"
|
||||
"time": "2026-08-24T10:51:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.38.0",
|
||||
"version": "v1.42.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
|
||||
"reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
|
||||
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502",
|
||||
"reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6075,7 +6067,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6095,7 +6087,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-25T13:48:31+00:00"
|
||||
"time": "2026-08-07T06:33:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
@@ -6751,16 +6743,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.4.13",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "f5804be144caceb570f6747519999636b664f24c"
|
||||
"reference": "058d17fc284cce14efb2385783b55014a461b176"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c",
|
||||
"reference": "f5804be144caceb570f6747519999636b664f24c",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176",
|
||||
"reference": "058d17fc284cce14efb2385783b55014a461b176",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6792,7 +6784,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.13"
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6812,7 +6804,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-23T16:05:06+00:00"
|
||||
"time": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/property-access",
|
||||
@@ -6897,16 +6889,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/property-info",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/property-info.git",
|
||||
"reference": "c79afdb2e720db8ee4f7050a07b3116b92cb896b"
|
||||
"reference": "a37837b52ed5dd7474c251d15694007344c6462d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/property-info/zipball/c79afdb2e720db8ee4f7050a07b3116b92cb896b",
|
||||
"reference": "c79afdb2e720db8ee4f7050a07b3116b92cb896b",
|
||||
"url": "https://api.github.com/repos/symfony/property-info/zipball/a37837b52ed5dd7474c251d15694007344c6462d",
|
||||
"reference": "a37837b52ed5dd7474c251d15694007344c6462d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6963,7 +6955,7 @@
|
||||
"validator"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/property-info/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/property-info/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6983,7 +6975,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-07T14:56:57+00:00"
|
||||
"time": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/rate-limiter",
|
||||
@@ -7061,16 +7053,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/routing",
|
||||
"version": "v7.4.15",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/routing.git",
|
||||
"reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b"
|
||||
"reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/80c0a93d3f8e7499f716204a1fb38ead942a7a2b",
|
||||
"reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/ddd558991e98f693ae6bf5063cc1b0362c6bbec3",
|
||||
"reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7122,7 +7114,7 @@
|
||||
"url"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/routing/tree/v7.4.15"
|
||||
"source": "https://github.com/symfony/routing/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7142,7 +7134,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-21T15:13:06+00:00"
|
||||
"time": "2026-08-17T13:12:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -7323,16 +7315,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/translation.git",
|
||||
"reference": "501e0ff4553c744209ca1a68790d8a4541563710"
|
||||
"reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/501e0ff4553c744209ca1a68790d8a4541563710",
|
||||
"reference": "501e0ff4553c744209ca1a68790d8a4541563710",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b",
|
||||
"reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7399,7 +7391,7 @@
|
||||
"description": "Provides tools to internationalize your application",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/translation/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/translation/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7419,7 +7411,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-30T12:37:26+00:00"
|
||||
"time": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation-contracts",
|
||||
@@ -7587,16 +7579,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/uid",
|
||||
"version": "v7.4.9",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/uid.git",
|
||||
"reference": "2676b524340abcfe4d6151ec698463cebafee439"
|
||||
"reference": "69d732355a139c6f8881337d28515aa01f12b8be"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439",
|
||||
"reference": "2676b524340abcfe4d6151ec698463cebafee439",
|
||||
"url": "https://api.github.com/repos/symfony/uid/zipball/69d732355a139c6f8881337d28515aa01f12b8be",
|
||||
"reference": "69d732355a139c6f8881337d28515aa01f12b8be",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7641,7 +7633,7 @@
|
||||
"uuid"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/uid/tree/v7.4.9"
|
||||
"source": "https://github.com/symfony/uid/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7661,20 +7653,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-30T15:19:22+00:00"
|
||||
"time": "2026-08-11T07:38:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/validator",
|
||||
"version": "v7.4.16",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/validator.git",
|
||||
"reference": "3aee77358ab14090337183657e554668bc94ab4a"
|
||||
"reference": "b1cbb758c005fbe0d7b2b8d1561869d318628a10"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/validator/zipball/3aee77358ab14090337183657e554668bc94ab4a",
|
||||
"reference": "3aee77358ab14090337183657e554668bc94ab4a",
|
||||
"url": "https://api.github.com/repos/symfony/validator/zipball/b1cbb758c005fbe0d7b2b8d1561869d318628a10",
|
||||
"reference": "b1cbb758c005fbe0d7b2b8d1561869d318628a10",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7745,7 +7737,7 @@
|
||||
"description": "Provides tools to validate values",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/validator/tree/v7.4.16"
|
||||
"source": "https://github.com/symfony/validator/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7765,7 +7757,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-06T09:41:15+00:00"
|
||||
"time": "2026-08-22T09:04:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-exporter",
|
||||
@@ -7849,16 +7841,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.4.15",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "e101850ded5d2c0d44bf32abb8996404afec2dec"
|
||||
"reference": "0b040d7b66ceb10b7bb24c8e6656257932693a12"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/e101850ded5d2c0d44bf32abb8996404afec2dec",
|
||||
"reference": "e101850ded5d2c0d44bf32abb8996404afec2dec",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/0b040d7b66ceb10b7bb24c8e6656257932693a12",
|
||||
"reference": "0b040d7b66ceb10b7bb24c8e6656257932693a12",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7901,7 +7893,7 @@
|
||||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.4.15"
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7921,7 +7913,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-21T15:13:06+00:00"
|
||||
"time": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "typo3/class-alias-loader",
|
||||
|
||||
80
migrations/check_usecase_markets.php
Normal file
80
migrations/check_usecase_markets.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Read-only diagnosis: why do success stories emit empty `markets`?
|
||||
*
|
||||
* Modeled on migrations/check_bg.php - PDO against the credentials from
|
||||
* config/system/settings.php, no TYPO3 bootstrap, SELECT only.
|
||||
*
|
||||
* Run on the server: php migrations/check_usecase_markets.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$settings = require __DIR__ . '/../config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $db['host'], $db['dbname']),
|
||||
$db['user'],
|
||||
$db['password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
function one(PDO $pdo, string $sql): array
|
||||
{
|
||||
return $pdo->query($sql)->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
function all(PDO $pdo, string $sql): array
|
||||
{
|
||||
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
echo "=== 1. MM-Tabellen: Zeilen ueberhaupt da? ===\n";
|
||||
foreach (['tx_vitec_usecase_market_mm', 'tx_vitec_usecase_solution_mm'] as $mm) {
|
||||
$n = one($pdo, "SELECT COUNT(*) c FROM {$mm}");
|
||||
echo sprintf("%-32s %s Zeilen\n", $mm, $n['c'] ?? '?');
|
||||
}
|
||||
|
||||
echo "\n=== 2. Waisen: MM -> Markets, die es nicht (mehr) gibt ===\n";
|
||||
echo "verknuepfte Market-uids und ihr Zustand:\n";
|
||||
$rows = all($pdo, "
|
||||
SELECT mm.uid_foreign AS market_uid,
|
||||
COUNT(*) AS stories,
|
||||
MAX(m.uid IS NULL) AS fehlt_ganz,
|
||||
MAX(COALESCE(m.deleted, -1)) AS deleted,
|
||||
MAX(COALESCE(m.hidden, -1)) AS hidden,
|
||||
MAX(COALESCE(m.title, '?')) AS title
|
||||
FROM tx_vitec_usecase_market_mm mm
|
||||
LEFT JOIN tx_vitec_domain_model_market m ON m.uid = mm.uid_foreign
|
||||
GROUP BY mm.uid_foreign
|
||||
ORDER BY mm.uid_foreign
|
||||
");
|
||||
foreach ($rows as $r) {
|
||||
$state = $r['fehlt_ganz'] ? 'FEHLT KOMPLETT' : (($r['deleted'] ?? 0) ? 'GELOESCHT' : ((($r['hidden'] ?? 0) ? 'versteckt' : 'ok')));
|
||||
echo sprintf(" market uid %-5s %-15s %-40s (%s Stories)\n", $r['market_uid'], $state, mb_substr((string)$r['title'], 0, 40), $r['stories']);
|
||||
}
|
||||
|
||||
echo "\n=== 3. Markets-Tabelle: Bestand ===\n";
|
||||
$m = one($pdo, "SELECT COUNT(*) total, SUM(deleted) del, SUM(hidden) hid, MIN(uid) minuid, MAX(uid) maxuid FROM tx_vitec_domain_model_market");
|
||||
echo sprintf("gesamt %s | geloescht %s | versteckt %s | uid-Bereich %s-%s\n", $m['total'], $m['del'], $m['hid'], $m['minuid'], $m['maxuid']);
|
||||
echo "geloeschte Markets (falls vorhanden):\n";
|
||||
foreach (all($pdo, "SELECT uid, title, FROM_UNIXTIME(tstamp) t FROM tx_vitec_domain_model_market WHERE deleted = 1 ORDER BY uid") as $r) {
|
||||
echo sprintf(" uid %-5s %-45s zuletzt %s\n", $r['uid'], mb_substr((string)$r['title'], 0, 45), $r['t']);
|
||||
}
|
||||
|
||||
echo "\n=== 4. sys_category-Zuordnungen der Stories (JSON-Feld `categories`) ===\n";
|
||||
$c = one($pdo, "SELECT COUNT(*) c FROM sys_category_record_mm WHERE tablenames = 'tx_vitec_domain_model_usecase'");
|
||||
echo "sys_category_record_mm fuer usecases: " . ($c['c'] ?? '?') . " Zeilen\n";
|
||||
|
||||
echo "\n=== 5. Stichprobe: 3 Stories mit ihren MM-Zeilen ===\n";
|
||||
foreach (all($pdo, "
|
||||
SELECT u.uid, u.title,
|
||||
(SELECT COUNT(*) FROM tx_vitec_usecase_market_mm mm WHERE mm.uid_local = u.uid) AS mmrows
|
||||
FROM tx_vitec_domain_model_usecase u
|
||||
WHERE u.deleted = 0
|
||||
ORDER BY u.uid LIMIT 3
|
||||
") as $r) {
|
||||
echo sprintf(" story uid %-4s %-40s -> %s MM-Zeile(n)\n", $r['uid'], mb_substr((string)$r['title'], 0, 40), $r['mmrows']);
|
||||
}
|
||||
|
||||
echo "\nFertig - nichts geschrieben.\n";
|
||||
@@ -40,6 +40,13 @@ foreach ($parsed['components'] as $c) {
|
||||
);
|
||||
}
|
||||
|
||||
echo "\n--- Composed sources (g:) ---\n";
|
||||
foreach (($reader->sources($parsed)['Composed'] ?? []) as $entry) {
|
||||
echo $entry['selector'] . "\n";
|
||||
echo " label: " . $entry['label'] . "\n";
|
||||
echo " html : " . $entry['value'] . "\n\n";
|
||||
}
|
||||
|
||||
echo "\n--- Related Product Cards, raw ---\n";
|
||||
foreach ($parsed['components'] as $c) {
|
||||
if ($c['component'] !== 'Related Product Card') {
|
||||
|
||||
BIN
migrations/products_xlsx/Aligo_Product Page Content.xlsx
Normal file
BIN
migrations/products_xlsx/Aligo_Product Page Content.xlsx
Normal file
Binary file not shown.
BIN
migrations/products_xlsx/Arqa_Product Page Content.xlsx
Normal file
BIN
migrations/products_xlsx/Arqa_Product Page Content.xlsx
Normal file
Binary file not shown.
51
migrations/reset_product_mapping.php
Normal file
51
migrations/reset_product_mapping.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-shot: clear the SAVED field mapping of the product-text import
|
||||
* (tx_vitec_import_mapping, model "product_xlsx") so the corrected
|
||||
* DEFAULT_MAPPING seed applies again.
|
||||
*
|
||||
* Deliberately clears ONLY the `mapping` column - the same row also stores
|
||||
* `record_aliases` (the editor's manual related-product matches), and those
|
||||
* must survive.
|
||||
*
|
||||
* Usage: php migrations/reset_product_mapping.php
|
||||
*/
|
||||
|
||||
$root = null;
|
||||
$dir = __DIR__;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if (is_file($dir . '/config/system/settings.php')) {
|
||||
$root = $dir;
|
||||
break;
|
||||
}
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if ($root === null) {
|
||||
exit("Could not locate project root above " . __DIR__ . "\n");
|
||||
}
|
||||
|
||||
$settings = require $root . '/config/system/settings.php';
|
||||
$db = $settings['DB']['Connections']['Default'];
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
|
||||
$db['user'] ?? '',
|
||||
$db['password'] ?? '',
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$row = $pdo->query("SELECT uid, mapping, record_aliases FROM tx_vitec_import_mapping WHERE model = 'product_xlsx'")->fetch();
|
||||
if (!$row) {
|
||||
exit("No row for model 'product_xlsx' - nothing to clear, defaults apply already.\n");
|
||||
}
|
||||
|
||||
echo "Row uid " . $row['uid'] . "\n";
|
||||
echo " mapping (cleared) : " . ($row['mapping'] ?: '(empty)') . "\n";
|
||||
echo " aliases (KEPT) : " . ($row['record_aliases'] ?: '(empty)') . "\n";
|
||||
|
||||
$pdo->prepare("UPDATE tx_vitec_import_mapping SET mapping = '', tstamp = ? WHERE model = 'product_xlsx'")
|
||||
->execute([time()]);
|
||||
|
||||
echo "Done - the saved field mapping is cleared, the corrected defaults apply on the next preview.\n";
|
||||
@@ -58,6 +58,25 @@ final class ImportSuccessStoriesCommand extends Command
|
||||
'accomodation' => 'Accommodation', 'accommodation' => 'Accommodation',
|
||||
];
|
||||
|
||||
/**
|
||||
* --markets-only: old industry names -> the nine main markets of the
|
||||
* current (SEO-CSV) taxonomy, decision 2026-08-26. The repair mode never
|
||||
* creates market records; a target title that does not exist is reported
|
||||
* as unresolved instead. Titles verified against the live records.
|
||||
*/
|
||||
private const INDUSTRY_TO_MARKET = [
|
||||
'Sports' => 'Sports, Venues & Entertainment',
|
||||
'Venues' => 'Sports, Venues & Entertainment',
|
||||
'Government' => 'Defense, Government & Public Sector',
|
||||
'Military' => 'Defense, Government & Public Sector',
|
||||
'Corporate' => 'Corporate & Enterprise',
|
||||
'Broadcast' => 'Media, Broadcast & Telecom',
|
||||
'Education' => 'Healthcare & Education',
|
||||
'Healthcare' => 'Healthcare & Education',
|
||||
'Hospitality & Leisure' => 'Retail, Hospitality & Leisure',
|
||||
'Accommodation' => 'Retail, Hospitality & Leisure',
|
||||
];
|
||||
|
||||
/** CTypes that never become content elements. */
|
||||
private const SKIP_CTYPES = [
|
||||
'shortcut', 'div', 'fluxbs5templates_buttonlink', 'mask_web__jumpmenu',
|
||||
@@ -88,6 +107,7 @@ final class ImportSuccessStoriesCommand extends Command
|
||||
$this->addOption('only', null, InputOption::VALUE_REQUIRED, 'Import only the story whose slug tail matches');
|
||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Analyse and report only, write nothing');
|
||||
$this->addOption('force', null, InputOption::VALUE_NONE, 'Delete + recreate stories that already exist (matched by slug)');
|
||||
$this->addOption('markets-only', null, InputOption::VALUE_NONE, 'Repair mode: only rewrite the markets relation of EXISTING records, touch nothing else');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -131,6 +151,10 @@ final class ImportSuccessStoriesCommand extends Command
|
||||
$dryRun ? 'DRY-RUN' : 'LIVE'
|
||||
));
|
||||
|
||||
if ((bool)$input->getOption('markets-only')) {
|
||||
return $this->relinkMarkets($stories, $only, $dryRun, $output);
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$this->ensureMarkets($output, $storagePid);
|
||||
}
|
||||
@@ -158,6 +182,110 @@ final class ImportSuccessStoriesCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* --markets-only repair mode. Re-derives each story's industries from the
|
||||
* export exactly like the full import, translates them onto the current
|
||||
* taxonomy (INDUSTRY_TO_MARKET) and rewrites ONLY the `markets` MM
|
||||
* relation of the existing usecase record - no other field is touched,
|
||||
* no record is created or deleted anywhere (unlike the full import this
|
||||
* mode never creates market records either). Stories whose target market
|
||||
* does not exist keep their current relations and are reported.
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $stories
|
||||
*/
|
||||
private function relinkMarkets(array $stories, string $only, bool $dryRun, OutputInterface $output): int
|
||||
{
|
||||
$marketConn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_market');
|
||||
foreach ($marketConn->select(['uid', 'title'], 'tx_vitec_domain_model_market', ['deleted' => 0])->fetchAllAssociative() as $marketRow) {
|
||||
$this->marketUidByTitle[$this->normTitle((string)$marketRow['title'])] = (int)$marketRow['uid'];
|
||||
}
|
||||
|
||||
$conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE);
|
||||
$datamap = [];
|
||||
$missing = 0;
|
||||
$unresolved = 0;
|
||||
|
||||
foreach ($stories as $page) {
|
||||
$slugTail = basename((string)$page['slug']);
|
||||
if ($only !== '' && $slugTail !== $only) {
|
||||
continue;
|
||||
}
|
||||
$slug = '/' . $slugTail;
|
||||
$card = $this->cardByPageUid[(int)$page['uid']] ?? null;
|
||||
$useCase = $this->useCaseByKey[$this->normTitle((string)$page['title'])] ?? null;
|
||||
$industries = $this->industriesForStory($page, $card, $useCase);
|
||||
|
||||
$existing = $conn->select(['uid'], self::TABLE, ['slug' => $slug, 'deleted' => 0])->fetchAssociative()
|
||||
?: $conn->select(['uid'], self::TABLE, ['slug' => $slugTail, 'deleted' => 0])->fetchAssociative();
|
||||
|
||||
if (!$existing) {
|
||||
$missing++;
|
||||
$output->writeln(sprintf(' <comment>%-52s</comment> no record for slug %s', $slugTail, $slug));
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets = array_values(array_unique(array_map(
|
||||
static fn(string $industry): string => self::INDUSTRY_TO_MARKET[$industry] ?? $industry,
|
||||
$industries
|
||||
)));
|
||||
$uids = array_values(array_unique(array_filter(array_map(
|
||||
fn(string $title): int => $this->marketUidByTitle[$this->normTitle($title)] ?? 0,
|
||||
$targets
|
||||
))));
|
||||
|
||||
if ($uids === []) {
|
||||
$unresolved++;
|
||||
$output->writeln(sprintf(
|
||||
' <comment>%-52s</comment> uid %-5d [%s] => [%s] -> unresolved, left untouched',
|
||||
$slugTail,
|
||||
(int)$existing['uid'],
|
||||
implode(',', $industries),
|
||||
implode(',', $targets)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf(
|
||||
' %-54s uid %-5d [%s] => [%s] -> markets %s',
|
||||
$slugTail,
|
||||
(int)$existing['uid'],
|
||||
implode(',', $industries),
|
||||
implode(',', $targets),
|
||||
implode(',', $uids)
|
||||
));
|
||||
$datamap[self::TABLE][(int)$existing['uid']] = ['markets' => implode(',', $uids)];
|
||||
}
|
||||
|
||||
$relinked = count($datamap[self::TABLE] ?? []);
|
||||
|
||||
if ($dryRun) {
|
||||
$output->writeln(sprintf(
|
||||
'<info>DRY-RUN: %d stories would be relinked, %d without record, %d unresolved.</info>',
|
||||
$relinked,
|
||||
$missing,
|
||||
$unresolved
|
||||
));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($datamap !== []) {
|
||||
$dh = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dh->start($datamap, []);
|
||||
$dh->process_datamap();
|
||||
foreach ($dh->errorLog as $error) {
|
||||
$output->writeln("<error>$error</error>");
|
||||
}
|
||||
}
|
||||
$output->writeln(sprintf(
|
||||
'<info>%d stories relinked, %d without record, %d unresolved.</info>',
|
||||
$relinked,
|
||||
$missing,
|
||||
$unresolved
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// =========================================================== index building
|
||||
|
||||
private function buildIndexes(): void
|
||||
@@ -268,7 +396,7 @@ final class ImportSuccessStoriesCommand extends Command
|
||||
}
|
||||
|
||||
/** Ensure one market record per canonical industry; fill title->uid map. */
|
||||
private function ensureMarkets(OutputInterface $output, int $fallbackPid): void
|
||||
private function ensureMarkets(OutputInterface $output, int $fallbackPid, bool $create = true): void
|
||||
{
|
||||
$conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_market');
|
||||
$rows = $conn->select(['uid', 'title', 'pid'], 'tx_vitec_domain_model_market', ['deleted' => 0])->fetchAllAssociative();
|
||||
@@ -285,6 +413,12 @@ final class ImportSuccessStoriesCommand extends Command
|
||||
}
|
||||
}
|
||||
if ($datamap !== []) {
|
||||
if (!$create) {
|
||||
foreach ($datamap['tx_vitec_domain_model_market'] as $fields) {
|
||||
$output->writeln(" <comment>would create market: {$fields['title']}</comment>");
|
||||
}
|
||||
return;
|
||||
}
|
||||
$dh = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dh->start($datamap, []);
|
||||
$dh->process_datamap();
|
||||
|
||||
@@ -173,6 +173,47 @@ final class ImportController
|
||||
return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecimport.seo'), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist hand-made record links from the SEO structure check. Shares the
|
||||
* per-model alias store with the import tabs (MappingRepository), so a
|
||||
* link made in either place serves both - and vitec:create-markets stops
|
||||
* proposing linked rows as new records. Key convention is the import
|
||||
* tab's: lowercased, trimmed CSV name. uid 0 removes a link.
|
||||
*/
|
||||
public function seoAliasesAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$body = (array)$request->getParsedBody();
|
||||
$modelKey = (string)($body['model'] ?? '');
|
||||
if ($this->registry->has($modelKey)) {
|
||||
$aliases = $this->mappingRepository->load($modelKey)['aliases'] ?? [];
|
||||
$names = is_array($body['aliaskey'] ?? null) ? $body['aliaskey'] : [];
|
||||
$selected = is_array($body['alias'] ?? null) ? $body['alias'] : [];
|
||||
$changed = 0;
|
||||
foreach ($selected as $index => $selectedUid) {
|
||||
$key = mb_strtolower(trim((string)($names[$index] ?? '')));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$uid = (int)$selectedUid;
|
||||
$current = (int)($aliases[$key] ?? 0);
|
||||
if ($uid > 0 && $uid !== $current) {
|
||||
$aliases[$key] = $uid;
|
||||
$changed++;
|
||||
} elseif ($uid === 0 && $current > 0) {
|
||||
unset($aliases[$key]);
|
||||
$changed++;
|
||||
}
|
||||
}
|
||||
$this->mappingRepository->saveAliases($modelKey, $aliases);
|
||||
$this->flash(
|
||||
sprintf('%d record link(s) changed for %s.', $changed, $modelKey),
|
||||
'Record links saved',
|
||||
true
|
||||
);
|
||||
}
|
||||
return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecimport.seo'), 303);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ rendering
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
@@ -50,28 +51,50 @@ final class ProductTextImportController
|
||||
/**
|
||||
* Never offered as import targets. `slug` changes URLs - that is redirect
|
||||
* territory, not a text import; the workbook URL is checked against the
|
||||
* record's slug informationally instead.
|
||||
* record's slug informationally instead. The flags and the shortcut pid are
|
||||
* editorial switches that will never come out of a workbook (decision
|
||||
* 2026-08-28) - a text import writing "1" into hideonwebsite would silently
|
||||
* unpublish a product. Only this XLSX flow filters them; the CSV flow keeps
|
||||
* the full field list.
|
||||
*/
|
||||
private const EXCLUDED_TARGETS = ['slug'];
|
||||
private const EXCLUDED_TARGETS = [
|
||||
'slug',
|
||||
'hideonapp',
|
||||
'hideonwebsite',
|
||||
'hideonproducts',
|
||||
'hideondatasheets',
|
||||
'shortcutpid',
|
||||
'shortcut',
|
||||
'legacy',
|
||||
'supportproduct',
|
||||
'subproduct',
|
||||
];
|
||||
|
||||
/**
|
||||
* Pre-seed for the very first mapping (no row in tx_vitec_import_mapping
|
||||
* yet). Editors change everything in the dropdowns; this only saves the
|
||||
* first manual pass. Keys are product columns, values source selectors as
|
||||
* ProductXlsxReader defines them.
|
||||
*
|
||||
* Verified against the finished MGW-Diamond-H page (2026-08-28): the hero
|
||||
* one-liner lives in `teaser` (subtitle is unused there), the ~45-70-word
|
||||
* introduction in `description`, the ~80-120-word overview in
|
||||
* `description2`, and the three Key Capability Groups land as ONE HTML
|
||||
* block in `capabilities` - hence the composed `g:` selector. Why-Choose
|
||||
* cards, resources, pre-footer and hero CTA are page content elements or
|
||||
* template copy on the finished page, not record fields, and therefore
|
||||
* have no defaults here. An earlier version of this seed mapped onto
|
||||
* `subtitle` and onto columns that exist only in the DB but not in the
|
||||
* TCA (`cta`, `key1-3`, `apptext1-3`) - both corrected.
|
||||
*/
|
||||
private const DEFAULT_MAPPING = [
|
||||
'subtitle' => 'c:Hero — H1#1:body',
|
||||
'teaser' => 'c:Product Introduction#1:body',
|
||||
'description' => 'c:Product Overview — H2#1:body',
|
||||
'cta' => 'c:Pre-footer CTA — H2#1:cta',
|
||||
'seotitle' => 'x:seotitle',
|
||||
'teaser' => 'c:Hero — H1#1:body',
|
||||
'description' => 'c:Product Introduction#1:body',
|
||||
'description2' => 'c:Product Overview — H2#1:body',
|
||||
'capabilities' => 'g:Key Capability Group',
|
||||
'textrelatedproducts' => 'p:Related Products — H2#1',
|
||||
'keywords' => 'm:primaryKeyword',
|
||||
'key1' => 'c:Why Choose Card#1:title',
|
||||
'apptext1' => 'c:Why Choose Card#1:body',
|
||||
'key2' => 'c:Why Choose Card#2:title',
|
||||
'apptext2' => 'c:Why Choose Card#2:body',
|
||||
'key3' => 'c:Why Choose Card#3:title',
|
||||
'apptext3' => 'c:Why Choose Card#3:body',
|
||||
];
|
||||
|
||||
private const SORTABLE = ['title', 'slug', 'tstamp'];
|
||||
@@ -252,6 +275,16 @@ final class ProductTextImportController
|
||||
|
||||
$map = array_map('strval', (array)($body['map'] ?? []));
|
||||
$apply = array_map('strval', (array)($body['apply'] ?? []));
|
||||
// A per-row "Save" button writes exactly its own row; the checkbox
|
||||
// state of every other row is deliberately ignored then. Only one
|
||||
// submit button ever posts its value, so the two can't both be set.
|
||||
$applySingle = trim((string)($body['applySingle'] ?? ''));
|
||||
$applyRelatedSingle = trim((string)($body['applyRelatedSingle'] ?? ''));
|
||||
if ($applySingle !== '') {
|
||||
$apply = [$applySingle];
|
||||
} elseif ($applyRelatedSingle !== '') {
|
||||
$apply = [];
|
||||
}
|
||||
$targets = $this->targetFields();
|
||||
|
||||
$data = [];
|
||||
@@ -271,6 +304,11 @@ final class ProductTextImportController
|
||||
// cannot be related to itself.
|
||||
$relatedChoices = array_map('intval', (array)($body['related'] ?? []));
|
||||
$applyRelated = array_map('intval', (array)($body['applyRelated'] ?? []));
|
||||
if ($applySingle !== '') {
|
||||
$applyRelated = [];
|
||||
} elseif ($applyRelatedSingle !== '') {
|
||||
$applyRelated = [(int)$applyRelatedSingle];
|
||||
}
|
||||
$relatedCards = $this->relatedCards($parsed);
|
||||
$relatedTodo = [];
|
||||
foreach ($applyRelated as $index) {
|
||||
@@ -352,6 +390,60 @@ final class ProductTextImportController
|
||||
return $this->renderTexts($request, $this->loadProduct($uid), $parsed, $map);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- inline field editing
|
||||
|
||||
/**
|
||||
* AJAX (vitec_product_field_get): the FULL raw value of one product field
|
||||
* for the click-to-edit cell - the cell itself only shows a truncated
|
||||
* preview. RTE fields return their stored HTML; editing is deliberately
|
||||
* source-level (decision 2026-08-28: no inline WYSIWYG).
|
||||
*/
|
||||
public function fieldGet(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
$uid = (int)($params['uid'] ?? 0);
|
||||
$field = (string)($params['field'] ?? '');
|
||||
$product = $this->loadProduct($uid);
|
||||
$targets = $this->targetFields();
|
||||
if ($product === null || !isset($targets[$field])) {
|
||||
return new JsonResponse(['success' => false, 'message' => 'Unknown product or field.']);
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'value' => (string)($product[$field] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX (vitec_product_field_save): write one product field. Same target
|
||||
* whitelist and the same DataHandler path as the form apply - RTE
|
||||
* transformations and record history included. Responds with the freshly
|
||||
* re-read value so the cell preview shows what was actually stored.
|
||||
*/
|
||||
public function fieldSave(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$body = (array)$request->getParsedBody();
|
||||
$uid = (int)($body['uid'] ?? 0);
|
||||
$field = (string)($body['field'] ?? '');
|
||||
$value = (string)($body['value'] ?? '');
|
||||
$targets = $this->targetFields();
|
||||
if ($this->loadProduct($uid) === null || !isset($targets[$field])) {
|
||||
return new JsonResponse(['success' => false, 'message' => 'Unknown product or field.']);
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([self::TABLE => [(string)$uid => [$field => $value]]], []);
|
||||
$dataHandler->process_datamap();
|
||||
if ($dataHandler->errorLog !== []) {
|
||||
return new JsonResponse(['success' => false, 'message' => implode(' | ', $dataHandler->errorLog)]);
|
||||
}
|
||||
$fresh = (string)($this->loadProduct($uid)[$field] ?? '');
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'value' => $fresh,
|
||||
'preview' => $this->preview($fresh),
|
||||
]);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ internals
|
||||
|
||||
/**
|
||||
@@ -439,6 +531,9 @@ final class ProductTextImportController
|
||||
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$this->pageRenderer->addCssFile('EXT:vitec/Resources/Public/Css/backend-import.css');
|
||||
// Click-to-edit for the "Current value" column; a module because the
|
||||
// backend CSP blocks inline handlers.
|
||||
$this->pageRenderer->loadJavaScriptModule('@evomedien/vitec/product-inline-edit.js');
|
||||
$view->assignMultiple([
|
||||
'product' => $product,
|
||||
'parsed' => $parsed,
|
||||
|
||||
@@ -144,7 +144,7 @@ final class ProductXlsxReader
|
||||
*/
|
||||
public function sources(array $parsed): array
|
||||
{
|
||||
$groups = ['Meta' => [], 'Components' => [], 'Page SEO Check' => []];
|
||||
$groups = ['Meta' => [], 'Components' => [], 'Composed' => [], 'Page SEO Check' => []];
|
||||
|
||||
foreach ($parsed['meta'] ?? [] as $key => $value) {
|
||||
$groups['Meta'][] = $this->entry('m:' . $key, 'Meta · ' . $key, (string)$value);
|
||||
@@ -169,6 +169,47 @@ final class ProductXlsxReader
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
// Section-intro rows ("... — H2") additionally as ONE ready RTE
|
||||
// value: heading plus paragraph in the exact markup the finished
|
||||
// MGW-Diamond product stores in `textrelatedproducts` - imported
|
||||
// and hand-written intros then render identically.
|
||||
if (str_ends_with($name, '— H2')) {
|
||||
$pair = $this->composePair($component);
|
||||
if ($pair !== null) {
|
||||
$groups['Composed'][] = $this->entry(
|
||||
'p:' . $name . '#' . $component['occurrence'],
|
||||
$name . $suffix . ' · Title+Body as HTML',
|
||||
$pair
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Composed sources: a component occurring several times can be pulled
|
||||
// as ONE value - every occurrence as <h3> + bullets/paragraph, prefixed
|
||||
// with the section heading. Built for the Key Capability Groups, whose
|
||||
// three rows land in the single `capabilities` field; that is the shape
|
||||
// the finished product pages already use.
|
||||
$counts = [];
|
||||
foreach ($parsed['components'] ?? [] as $component) {
|
||||
$counts[(string)$component['component']] = ((int)($component['occurrence'] ?? 1));
|
||||
}
|
||||
foreach ($counts as $name => $count) {
|
||||
if ($count < 2 || in_array($name, self::HIDDEN_COMPONENTS, true)) {
|
||||
continue;
|
||||
}
|
||||
$html = $this->composeGroup($parsed, $name);
|
||||
if ($html !== null) {
|
||||
$groups['Composed'][] = $this->entry('g:' . $name, $name . ' · all ' . $count . ' as HTML', $html);
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-component composite for the SEO title: hero title, em dash,
|
||||
// introduction heading - "Aligo — High-performance AV over IP and KVM".
|
||||
$seoTitle = $this->composeSeoTitle($parsed);
|
||||
if ($seoTitle !== null) {
|
||||
$groups['Composed'][] = $this->entry('x:seotitle', 'Hero title — Intro heading (SEO title)', $seoTitle);
|
||||
}
|
||||
|
||||
foreach ($parsed['seo'] ?? [] as $header => $value) {
|
||||
@@ -191,6 +232,20 @@ final class ProductXlsxReader
|
||||
$value = $parsed['meta'][substr($selector, 2)] ?? null;
|
||||
return is_string($value) && $value !== '' ? $value : null;
|
||||
}
|
||||
if (str_starts_with($selector, 'g:')) {
|
||||
return $this->composeGroup($parsed, substr($selector, 2));
|
||||
}
|
||||
if (str_starts_with($selector, 'p:') && preg_match('/^p:(.+)#(\d+)$/', $selector, $m)) {
|
||||
foreach ($parsed['components'] ?? [] as $component) {
|
||||
if ((string)$component['component'] === $m[1] && (int)$component['occurrence'] === (int)$m[2]) {
|
||||
return $this->composePair($component);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if ($selector === 'x:seotitle') {
|
||||
return $this->composeSeoTitle($parsed);
|
||||
}
|
||||
if (str_starts_with($selector, 's:')) {
|
||||
$value = $parsed['seo'][substr($selector, 2)] ?? null;
|
||||
return is_string($value) && $value !== '' ? $value : null;
|
||||
@@ -206,6 +261,121 @@ final class ProductXlsxReader
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO title composite: the hero title (the product name), an em dash, and
|
||||
* the introduction heading. Plain text - `seotitle` is a plain input, no
|
||||
* escaping wanted here.
|
||||
*
|
||||
* @param array<string,mixed> $parsed
|
||||
*/
|
||||
private function composeSeoTitle(array $parsed): ?string
|
||||
{
|
||||
$hero = null;
|
||||
$intro = null;
|
||||
foreach ($parsed['components'] ?? [] as $component) {
|
||||
$name = (string)($component['component'] ?? '');
|
||||
if ($name === 'Hero — H1' && (int)($component['occurrence'] ?? 1) === 1) {
|
||||
$hero = trim((string)($component['title'] ?? ''));
|
||||
} elseif ($name === 'Product Introduction' && (int)($component['occurrence'] ?? 1) === 1) {
|
||||
$intro = trim((string)($component['title'] ?? ''));
|
||||
}
|
||||
}
|
||||
if ($hero === null || $hero === '' || $intro === null || $intro === '') {
|
||||
return null;
|
||||
}
|
||||
return $hero . ' — ' . $intro;
|
||||
}
|
||||
|
||||
/**
|
||||
* One component row as a heading/paragraph pair. The markup - centred h3
|
||||
* with an inner span, centred p - is copied verbatim from what the
|
||||
* finished MGW-Diamond product carries in `textrelatedproducts`, so the
|
||||
* front end renders imported intros exactly like the hand-made one.
|
||||
*
|
||||
* @param array<string,mixed> $component
|
||||
*/
|
||||
private function composePair(array $component): ?string
|
||||
{
|
||||
$title = trim((string)($component['title'] ?? ''));
|
||||
$body = trim((string)($component['body'] ?? ''));
|
||||
if ($body === '') {
|
||||
$body = trim((string)($component['cta'] ?? ''));
|
||||
}
|
||||
$body = trim((string)preg_replace('/\|\s*CTA:.*$/su', '', $body));
|
||||
if ($title === '' || $body === '') {
|
||||
return null;
|
||||
}
|
||||
return '<h3 class="text-center"><span>' . htmlspecialchars($title, ENT_QUOTES) . '</span></h3>' . "\n"
|
||||
. '<p class="text-center">' . htmlspecialchars($body, ENT_QUOTES) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* All occurrences of one component, composed into a single HTML block in
|
||||
* the exact markup the hand-made MGW-Diamond `capabilities` field uses
|
||||
* (the front end styles only that shape): the immediately preceding
|
||||
* "... — H2" row (found by ORDER, not by name - "Key Capability Group" vs
|
||||
* "Key Capabilities — H2" makes name matching fragile) becomes
|
||||
* <h3 class="text-center"><span>, each occurrence's title a
|
||||
* <p><strong> group heading, and a body whose text uses "•" bullets
|
||||
* becomes a <ul>. Plain text otherwise.
|
||||
*
|
||||
* @param array<string,mixed> $parsed
|
||||
*/
|
||||
private function composeGroup(array $parsed, string $name): ?string
|
||||
{
|
||||
$components = $parsed['components'] ?? [];
|
||||
$members = array_values(array_filter(
|
||||
$components,
|
||||
static fn(array $c): bool => (string)($c['component'] ?? '') === $name
|
||||
));
|
||||
if ($members === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$firstOrder = (int)($members[0]['order'] ?? 0);
|
||||
foreach ($components as $component) {
|
||||
if ((int)($component['order'] ?? 0) === $firstOrder - 1
|
||||
&& str_ends_with((string)($component['component'] ?? ''), '— H2')
|
||||
&& trim((string)($component['title'] ?? '')) !== ''
|
||||
) {
|
||||
$html .= '<h3 class="text-center"><span>'
|
||||
. htmlspecialchars(trim((string)$component['title']), ENT_QUOTES)
|
||||
. '</span></h3>' . "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($members as $member) {
|
||||
$title = trim((string)($member['title'] ?? ''));
|
||||
$body = trim((string)($member['body'] ?? ''));
|
||||
if ($body === '') {
|
||||
$body = trim((string)($member['cta'] ?? ''));
|
||||
}
|
||||
// Card copy carries a "| CTA: <label>" suffix; the label is
|
||||
// display-only everywhere else and would be junk inside a <p>.
|
||||
$body = trim((string)preg_replace('/\|\s*CTA:.*$/su', '', $body));
|
||||
if ($title !== '') {
|
||||
$html .= '<p><strong>' . htmlspecialchars($title, ENT_QUOTES) . '</strong></p>';
|
||||
}
|
||||
if ($body === '') {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($body, '•')) {
|
||||
$items = array_filter(array_map('trim', explode('•', $body)));
|
||||
$html .= '<ul>';
|
||||
foreach ($items as $item) {
|
||||
$html .= '<li>' . htmlspecialchars($item, ENT_QUOTES) . '</li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
} else {
|
||||
$html .= '<p>' . htmlspecialchars($body, ENT_QUOTES) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
return $html !== '' ? $html : null;
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $components */
|
||||
private function detectPageType(array $components): string
|
||||
{
|
||||
|
||||
@@ -18,7 +18,10 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
* products -> level-3.x.y+ rows against tx_vitec_domain_model_product
|
||||
* (3.x rows are categories, not products - skipped)
|
||||
* Record matching: slug == last URL segment, falling back to a
|
||||
* normalized title comparison (parentheses stripped, & -> and).
|
||||
* normalized title comparison (parentheses stripped, & -> and), and
|
||||
* finally the hand-made record links (record_aliases per model in
|
||||
* tx_vitec_import_mapping - the same store the import tabs use, so
|
||||
* one link serves both views and vitec:create-markets).
|
||||
* - diff against the previous delivery, keyed by URL
|
||||
*
|
||||
* Works on the normalized row schema produced by normalizeRows(). Read-only:
|
||||
@@ -141,7 +144,8 @@ final class SeoResearchService
|
||||
$depth = substr_count($r['ref'], '.') + 1;
|
||||
return $r['section'] === $section && $depth >= $cfg['depth'][0] && $depth <= $cfg['depth'][1];
|
||||
}));
|
||||
$out[$cfg['key']] = $this->recordsCheck($candidates, $cfg['table']);
|
||||
$aliases = GeneralUtility::makeInstance(MappingRepository::class)->load($cfg['key'])['aliases'] ?? [];
|
||||
$out[$cfg['key']] = $this->recordsCheck($candidates, $cfg['table'], $aliases);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
@@ -174,9 +178,10 @@ final class SeoResearchService
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,string>> $candidates
|
||||
* @return array{total:int,matched:int,missing:array<int,array<string,string>>,extra:array<int,array<string,string>>}
|
||||
* @param array<string,int> $aliases lowercased CSV name -> record uid
|
||||
* @return array{total:int,matched:int,both:array<int,array<string,string>>,missing:array<int,array<string,string>>,extra:array<int,array<string,string>>,linkable:array<int,array<string,string>>}
|
||||
*/
|
||||
private function recordsCheck(array $candidates, string $table): array
|
||||
private function recordsCheck(array $candidates, string $table, array $aliases): array
|
||||
{
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
||||
$records = $qb->select('uid', 'title', 'slug')->from($table)
|
||||
@@ -185,28 +190,43 @@ final class SeoResearchService
|
||||
|
||||
$bySlug = [];
|
||||
$byTitle = [];
|
||||
$byUid = [];
|
||||
foreach ($records as $rec) {
|
||||
$slug = mb_strtolower(trim((string)($rec['slug'] ?? '')));
|
||||
if ($slug !== '') {
|
||||
$bySlug[$slug] = $rec;
|
||||
}
|
||||
$byTitle[$this->normalizeTitle((string)$rec['title'])] = $rec;
|
||||
$byUid[(int)$rec['uid']] = $rec;
|
||||
}
|
||||
|
||||
$missing = [];
|
||||
$both = [];
|
||||
$matchedUids = [];
|
||||
$directUids = [];
|
||||
foreach ($candidates as $r) {
|
||||
$segment = mb_strtolower(trim((string)basename(rtrim($r['url'], '/'))));
|
||||
$rec = $bySlug[$segment] ?? $byTitle[$this->normalizeTitle($r['name'])] ?? null;
|
||||
$via = '';
|
||||
if ($rec === null) {
|
||||
$aliasUid = (int)($aliases[mb_strtolower(trim($r['name']))] ?? 0);
|
||||
if ($aliasUid > 0 && isset($byUid[$aliasUid])) {
|
||||
$rec = $byUid[$aliasUid];
|
||||
$via = 'link';
|
||||
}
|
||||
}
|
||||
if ($rec !== null) {
|
||||
$matchedUids[(int)$rec['uid']] = true;
|
||||
if ($via === '') {
|
||||
$directUids[(int)$rec['uid']] = true;
|
||||
}
|
||||
$both[] = [
|
||||
'ref' => $r['ref'],
|
||||
'name' => $r['name'],
|
||||
'url' => $r['url'],
|
||||
'uid' => (string)$rec['uid'],
|
||||
'title' => (string)$rec['title'],
|
||||
'via' => $via,
|
||||
];
|
||||
} else {
|
||||
$missing[] = $r;
|
||||
@@ -214,11 +234,19 @@ final class SeoResearchService
|
||||
}
|
||||
|
||||
$extra = [];
|
||||
$linkable = [];
|
||||
foreach ($records as $rec) {
|
||||
if (!isset($matchedUids[(int)$rec['uid']])) {
|
||||
$extra[] = ['uid' => (string)$rec['uid'], 'title' => (string)$rec['title']];
|
||||
}
|
||||
// Select options for the hand-made links: everything the direct
|
||||
// match (slug/title) did not claim - alias-linked records stay in
|
||||
// here so a linked row can be re-pointed or unlinked.
|
||||
if (!isset($directUids[(int)$rec['uid']])) {
|
||||
$linkable[] = ['uid' => (string)$rec['uid'], 'title' => (string)$rec['title']];
|
||||
}
|
||||
}
|
||||
usort($linkable, static fn(array $a, array $b): int => strcasecmp($a['title'], $b['title']));
|
||||
|
||||
return [
|
||||
'total' => count($candidates),
|
||||
@@ -226,6 +254,7 @@ final class SeoResearchService
|
||||
'both' => $both,
|
||||
'missing' => $missing,
|
||||
'extra' => $extra,
|
||||
'linkable' => $linkable,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\PageTitle;
|
||||
|
||||
use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
|
||||
/**
|
||||
* Page title for the success-story detail page - without this every story
|
||||
* answers with the generic page title "Story".
|
||||
*
|
||||
* Singleton on purpose, same reasoning as ProductPageTitleProvider: the
|
||||
* UsecaseShowJsonRenderer that sets the title and the
|
||||
* PageTitleProviderManager that later reads it both obtain the provider via
|
||||
* GeneralUtility::makeInstance(); without the singleton those are two
|
||||
* different instances and the title is never seen by the manager.
|
||||
*/
|
||||
final class UsecasePageTitleProvider extends AbstractPageTitleProvider implements SingletonInterface
|
||||
{
|
||||
private string $seotitle = '';
|
||||
|
||||
public function setSeoTitle(string $seotitle): void
|
||||
{
|
||||
$this->seotitle = $seotitle;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->seotitle;
|
||||
}
|
||||
}
|
||||
35
packages/vitec/Classes/Seo/VitecMetaHandler.php
Normal file
35
packages/vitec/Classes/Seo/VitecMetaHandler.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Seo;
|
||||
|
||||
use FriendsOfTYPO3\Headless\Seo\MetaHandler;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Mirrors the real page <title> into `meta.title` of the page JSON.
|
||||
*
|
||||
* Why: `meta.title` is rendered from TypoScript (lib.meta) BEFORE the page
|
||||
* content, so on product/usecase detail pages it still shows the generic
|
||||
* page title ("Product") - the record's seotitle only reaches the
|
||||
* PageTitleProvider while the content renders. Headless fixes `seo.title`
|
||||
* afterwards through this handler; we extend it so `meta.title` gets the
|
||||
* same final value. Registered via the MetaHandlerInterface alias in
|
||||
* Services.yaml, which covers both the cacheable listener and the
|
||||
* USER_INT middleware path.
|
||||
*/
|
||||
class VitecMetaHandler extends MetaHandler
|
||||
{
|
||||
public function process(ServerRequestInterface $request, array $content): array
|
||||
{
|
||||
$content = parent::process($request, $content);
|
||||
|
||||
$title = trim((string)($content['seo']['title'] ?? ''));
|
||||
if ($title !== '' && is_array($content['meta'] ?? null)) {
|
||||
$content['meta']['title'] = $title;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,15 @@ class UsecaseShowJsonRenderer
|
||||
: '';
|
||||
}
|
||||
|
||||
$pageTitle = trim((string)($usecase['seo_title'] ?? ''));
|
||||
if ($pageTitle === '') {
|
||||
$pageTitle = trim((string)($usecase['title'] ?? ''));
|
||||
}
|
||||
if ($pageTitle !== '') {
|
||||
GeneralUtility::makeInstance(\Evomedien\Vitec\PageTitle\UsecasePageTitleProvider::class)
|
||||
->setSeoTitle($pageTitle);
|
||||
}
|
||||
|
||||
$serializer = GeneralUtility::makeInstance(UsecaseSerializer::class);
|
||||
|
||||
$backPid = (int)($settings['backPid'] ?? 0);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Evomedien\Vitec\Controller\Backend\ProductTextImportController;
|
||||
use Evomedien\Vitec\Controller\Backend\StructuredDataController;
|
||||
|
||||
/**
|
||||
@@ -16,4 +17,14 @@ return [
|
||||
'path' => '/vitec/structureddata/generate',
|
||||
'target' => StructuredDataController::class . '::generate',
|
||||
],
|
||||
// Click-to-edit in the product text import's "Current value" column:
|
||||
// read the full raw value / write one field through DataHandler.
|
||||
'vitec_product_field_get' => [
|
||||
'path' => '/vitec/product/field/get',
|
||||
'target' => ProductTextImportController::class . '::fieldGet',
|
||||
],
|
||||
'vitec_product_field_save' => [
|
||||
'path' => '/vitec/product/field/save',
|
||||
'target' => ProductTextImportController::class . '::fieldSave',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -35,6 +35,10 @@ return [
|
||||
'target' => ImportController::class . '::seoUploadAction',
|
||||
'methods' => ['POST'],
|
||||
],
|
||||
'seo_aliases' => [
|
||||
'target' => ImportController::class . '::seoAliasesAction',
|
||||
'methods' => ['POST'],
|
||||
],
|
||||
'products' => [
|
||||
'target' => ProductTextImportController::class . '::productsAction',
|
||||
],
|
||||
|
||||
@@ -16,3 +16,9 @@ services:
|
||||
TYPO3\CMS\Backend\View\BackendLayoutView:
|
||||
alias: Evomedien\Vitec\View\VitecBackendLayoutView
|
||||
public: true
|
||||
|
||||
# Headless resolves the meta handler via this interface (cacheable listener
|
||||
# AND the USER_INT middleware). Pointing the alias at our subclass mirrors
|
||||
# the final page title (seo.title) into meta.title as well.
|
||||
FriendsOfTYPO3\Headless\Seo\MetaHandlerInterface:
|
||||
alias: Evomedien\Vitec\Seo\VitecMetaHandler
|
||||
|
||||
@@ -7,6 +7,11 @@ config {
|
||||
before = record
|
||||
before = seo
|
||||
}
|
||||
vitecUsecase {
|
||||
provider = Evomedien\Vitec\PageTitle\UsecasePageTitleProvider
|
||||
before = record
|
||||
before = seo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,11 @@ fields:
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: mp4,webm,ogv,mov,m4v
|
||||
- identifier: video_poster
|
||||
type: File
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: common-image-types
|
||||
- identifier: link
|
||||
type: Link
|
||||
allowedTypes:
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<trans-unit id="items.video.label">
|
||||
<source>Video</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="items.video_poster.label">
|
||||
<source>Video Poster Image</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="items.link.label">
|
||||
<source>Link</source>
|
||||
</trans-unit>
|
||||
|
||||
@@ -78,6 +78,12 @@ fields:
|
||||
maxitems: 1
|
||||
allowed: mp4,webm,ogv,mov,m4v
|
||||
|
||||
- identifier: featured_video_poster
|
||||
type: File
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: common-image-types
|
||||
|
||||
- identifier: featured_video_autoplay
|
||||
type: Checkbox
|
||||
default: 0
|
||||
|
||||
@@ -61,6 +61,12 @@
|
||||
<trans-unit id="featured_video_muted.label">
|
||||
<source>Mute video</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="featured_video_poster.label">
|
||||
<source>Video Poster Image</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="featured_video_poster.description">
|
||||
<source>Preview image shown before the video plays.</source>
|
||||
</trans-unit>
|
||||
|
||||
<!-- Overlay -->
|
||||
<trans-unit id="overlay_color.label">
|
||||
|
||||
@@ -135,6 +135,12 @@ fields:
|
||||
maxitems: 1
|
||||
allowed: mp4,webm,ogv,mov,m4v
|
||||
|
||||
- identifier: hero_video_poster
|
||||
type: File
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: common-image-types
|
||||
|
||||
- identifier: hero_video_alignment
|
||||
type: Select
|
||||
renderType: selectSingle
|
||||
@@ -170,6 +176,12 @@ fields:
|
||||
maxitems: 1
|
||||
allowed: mp4,webm,ogv,mov,m4v
|
||||
|
||||
- identifier: hero_bgvideo_poster
|
||||
type: File
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: common-image-types
|
||||
|
||||
- identifier: hero_overlay_color
|
||||
type: Color
|
||||
label: Overlay Color
|
||||
|
||||
@@ -128,6 +128,18 @@
|
||||
<trans-unit id="hero_bgvideo.description">
|
||||
<source>Fullscreen background video behind the hero content.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_video_poster.label">
|
||||
<source>Video Poster Image</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_video_poster.description">
|
||||
<source>Preview image shown before the video plays.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_bgvideo_poster.label">
|
||||
<source>Background Video Poster Image</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_bgvideo_poster.description">
|
||||
<source>Preview image shown before the background video plays.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="hero_overlay_color.label">
|
||||
<source>Overlay Color</source>
|
||||
</trans-unit>
|
||||
|
||||
@@ -77,6 +77,12 @@ fields:
|
||||
maxitems: 1
|
||||
allowed: mp4,webm,ogv,mov,m4v
|
||||
|
||||
- identifier: intro_video_poster
|
||||
type: File
|
||||
minitems: 0
|
||||
maxitems: 1
|
||||
allowed: common-image-types
|
||||
|
||||
- identifier: intro_video_alignment
|
||||
type: Select
|
||||
renderType: selectSingle
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
<trans-unit id="intro_video_autoplay.label"><source>Autoplay</source></trans-unit>
|
||||
<trans-unit id="intro_video_loop.label"><source>Loop</source></trans-unit>
|
||||
<trans-unit id="intro_video_muted.label"><source>Muted</source></trans-unit>
|
||||
<trans-unit id="intro_video_poster.label"><source>Video Poster Image</source></trans-unit>
|
||||
<trans-unit id="intro_video_poster.description"><source>Preview image shown before the video plays.</source></trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -79,7 +79,10 @@
|
||||
</f:for>
|
||||
</select>
|
||||
</td>
|
||||
<td><small>{row.oldPreview}</small></td>
|
||||
<td data-vitec-inline-edit="1" data-uid="{product.uid}" data-field="{row.field}"
|
||||
style="cursor:pointer;" title="Click to edit this field">
|
||||
<small data-vitec-preview="1">{row.oldPreview}</small>
|
||||
</td>
|
||||
<td>
|
||||
<f:if condition="{row.new}">
|
||||
<f:then>
|
||||
@@ -96,6 +99,10 @@
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="apply[]" value="{row.field}"
|
||||
{f:if(condition: row.changed, then: 'checked="checked"')} />
|
||||
<button type="submit" class="btn btn-sm btn-default" style="margin-left:.5rem;"
|
||||
name="applySingle" value="{row.field}"
|
||||
formaction="{f:be.uri(route: 'web_vitecimport.product_apply')}"
|
||||
title="Write only this field now">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</f:for>
|
||||
@@ -152,6 +159,10 @@
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="applyRelated[]" value="{rel.index}"
|
||||
{f:if(condition: rel.changed, then: 'checked="checked"')} />
|
||||
<button type="submit" class="btn btn-sm btn-default" style="margin-left:.5rem;"
|
||||
name="applyRelatedSingle" value="{rel.index}"
|
||||
formaction="{f:be.uri(route: 'web_vitecimport.product_apply')}"
|
||||
title="Write only this card now">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</f:for>
|
||||
|
||||
@@ -91,6 +91,10 @@
|
||||
sub-solutions. Products: level x.y and deeper (the top product rows are
|
||||
categories). Matching: record slug against the last URL segment, falling back
|
||||
to a normalized title comparison. The Page Ref column shows the hierarchy.
|
||||
A row the matcher misses can be <strong>linked by hand</strong> via its select
|
||||
— the link is stored per model (shared with the import tabs) and re-applied
|
||||
on every future delivery. Linked rows show up under "Both" with a badge
|
||||
and can be re-pointed or removed there.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
@@ -98,22 +102,52 @@
|
||||
<f:if condition="{area} != 'pages'">
|
||||
<div class="col-md-4">
|
||||
<h4 style="text-transform:capitalize;">{area}</h4>
|
||||
<form action="{f:be.uri(route: 'web_vitecimport.seo_aliases')}" method="post">
|
||||
<input type="hidden" name="model" value="{area}" />
|
||||
<f:if condition="{check.both}">
|
||||
<p><strong>Both in CSV and TYPO3 ({check.both -> f:count()})</strong></p>
|
||||
<ul>
|
||||
<f:for each="{check.both}" as="pair">
|
||||
<li><code>{pair.ref}</code> {pair.name} <small class="text-muted">→ uid {pair.uid} ({pair.title})</small></li>
|
||||
<f:for each="{check.both}" as="pair" iteration="bIt">
|
||||
<li>
|
||||
<code>{pair.ref}</code> {pair.name}
|
||||
<small class="text-muted">→ uid {pair.uid} ({pair.title})</small>
|
||||
<f:if condition="{pair.via} == 'link'">
|
||||
<span class="vitec-badge vitec-badge-info">linked</span>
|
||||
<input type="hidden" name="aliaskey[b{bIt.index}]" value="{pair.name}" />
|
||||
<select name="alias[b{bIt.index}]" class="form-select form-select-sm" style="margin:2px 0 6px;">
|
||||
<option value="0">— remove link —</option>
|
||||
<f:for each="{check.linkable}" as="rec">
|
||||
<option value="{rec.uid}" {f:if(condition: '{rec.uid} == {pair.uid}', then: 'selected')}>{rec.title} (uid {rec.uid})</option>
|
||||
</f:for>
|
||||
</select>
|
||||
</f:if>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</f:if>
|
||||
<f:if condition="{check.missing}">
|
||||
<p><strong>Missing in TYPO3 ({check.missing -> f:count()})</strong></p>
|
||||
<ul>
|
||||
<f:for each="{check.missing}" as="row">
|
||||
<li><code>{row.ref}</code> {row.name}</li>
|
||||
<f:for each="{check.missing}" as="row" iteration="mIt">
|
||||
<li>
|
||||
<code>{row.ref}</code> {row.name}
|
||||
<f:if condition="{check.linkable}">
|
||||
<input type="hidden" name="aliaskey[m{mIt.index}]" value="{row.name}" />
|
||||
<select name="alias[m{mIt.index}]" class="form-select form-select-sm" style="margin:2px 0 6px;">
|
||||
<option value="0">— really missing —</option>
|
||||
<f:for each="{check.linkable}" as="rec">
|
||||
<option value="{rec.uid}">{rec.title} (uid {rec.uid})</option>
|
||||
</f:for>
|
||||
</select>
|
||||
</f:if>
|
||||
</li>
|
||||
</f:for>
|
||||
</ul>
|
||||
</f:if>
|
||||
<f:if condition="{check.missing} || {check.both}">
|
||||
<p><button type="submit" class="btn btn-default btn-sm">Save record links</button></p>
|
||||
</f:if>
|
||||
</form>
|
||||
<f:if condition="{check.extra}">
|
||||
<p><strong>Only in TYPO3 ({check.extra -> f:count()})</strong></p>
|
||||
<ul>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* VITEC – click-to-edit for the "Current value" column of the product text
|
||||
* import (Edit Product view).
|
||||
* EXT:vitec/Resources/Public/Javascript/product-inline-edit.js
|
||||
*
|
||||
* Delegated click handler on [data-vitec-inline-edit] cells (backend CSP
|
||||
* forbids inline handlers). Clicking a cell fetches the FULL raw field value
|
||||
* (the cell itself only shows a truncated preview), swaps in a textarea with
|
||||
* Save/Cancel, and writes through the vitec_product_field_save AJAX route,
|
||||
* which uses the same whitelist and DataHandler path as the form apply.
|
||||
* RTE fields are edited as raw HTML on purpose - no inline WYSIWYG
|
||||
* (decision 2026-08-28).
|
||||
*/
|
||||
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";
|
||||
import Notification from "@typo3/backend/notification.js";
|
||||
|
||||
const SELECTOR = "[data-vitec-inline-edit]";
|
||||
|
||||
function previewOf(cell) {
|
||||
return cell.querySelector("[data-vitec-preview]");
|
||||
}
|
||||
|
||||
function closeEditor(cell) {
|
||||
const editor = cell.querySelector("[data-vitec-editor]");
|
||||
if (editor) {
|
||||
editor.remove();
|
||||
}
|
||||
const preview = previewOf(cell);
|
||||
if (preview) {
|
||||
preview.hidden = false;
|
||||
}
|
||||
delete cell.dataset.vitecEditing;
|
||||
}
|
||||
|
||||
async function openEditor(cell) {
|
||||
if (cell.dataset.vitecEditing === "1") {
|
||||
return;
|
||||
}
|
||||
cell.dataset.vitecEditing = "1";
|
||||
|
||||
const getUrl = TYPO3.settings.ajaxUrls["vitec_product_field_get"];
|
||||
const saveUrl = TYPO3.settings.ajaxUrls["vitec_product_field_save"];
|
||||
if (!getUrl || !saveUrl) {
|
||||
Notification.error("Inline edit", "AJAX routes are not registered.");
|
||||
delete cell.dataset.vitecEditing;
|
||||
return;
|
||||
}
|
||||
|
||||
let value = "";
|
||||
try {
|
||||
const response = await new AjaxRequest(getUrl)
|
||||
.withQueryArguments({ uid: cell.dataset.uid, field: cell.dataset.field })
|
||||
.get();
|
||||
const data = await response.resolve();
|
||||
if (!data.success) {
|
||||
Notification.error("Inline edit", data.message || "Could not load the field value.");
|
||||
delete cell.dataset.vitecEditing;
|
||||
return;
|
||||
}
|
||||
value = data.value;
|
||||
} catch (e) {
|
||||
Notification.error("Inline edit", "Loading the field value failed.");
|
||||
delete cell.dataset.vitecEditing;
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = previewOf(cell);
|
||||
if (preview) {
|
||||
preview.hidden = true;
|
||||
}
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.setAttribute("data-vitec-editor", "1");
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.className = "form-control";
|
||||
textarea.rows = Math.min(14, Math.max(3, Math.ceil(value.length / 80)));
|
||||
textarea.style.fontFamily = "monospace";
|
||||
textarea.style.fontSize = "11px";
|
||||
textarea.value = value;
|
||||
wrap.appendChild(textarea);
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.style.marginTop = ".25rem";
|
||||
|
||||
// type="button" is essential: the cells live inside the big mapping <form>,
|
||||
// a bare <button> would submit it.
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.type = "button";
|
||||
saveBtn.className = "btn btn-sm btn-primary";
|
||||
saveBtn.textContent = "Save";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.type = "button";
|
||||
cancelBtn.className = "btn btn-sm btn-default";
|
||||
cancelBtn.style.marginLeft = ".5rem";
|
||||
cancelBtn.textContent = "Cancel";
|
||||
|
||||
bar.appendChild(saveBtn);
|
||||
bar.appendChild(cancelBtn);
|
||||
wrap.appendChild(bar);
|
||||
cell.appendChild(wrap);
|
||||
textarea.focus();
|
||||
|
||||
cancelBtn.addEventListener("click", () => closeEditor(cell));
|
||||
textarea.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeEditor(cell);
|
||||
}
|
||||
});
|
||||
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const response = await new AjaxRequest(saveUrl).post({
|
||||
uid: cell.dataset.uid,
|
||||
field: cell.dataset.field,
|
||||
value: textarea.value,
|
||||
});
|
||||
const data = await response.resolve();
|
||||
if (!data.success) {
|
||||
Notification.error("Inline edit", data.message || "Saving failed.");
|
||||
saveBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (preview) {
|
||||
preview.textContent = data.preview;
|
||||
}
|
||||
closeEditor(cell);
|
||||
Notification.success("Inline edit", cell.dataset.field + " saved.");
|
||||
} catch (e) {
|
||||
Notification.error("Inline edit", "Saving failed.");
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const cell = event.target.closest(SELECTOR);
|
||||
if (cell === null) {
|
||||
return;
|
||||
}
|
||||
// Clicks on the editor's own controls must not re-open it.
|
||||
if (event.target.closest("[data-vitec-editor]")) {
|
||||
return;
|
||||
}
|
||||
openEditor(cell);
|
||||
});
|
||||
@@ -25,8 +25,8 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<title>VITEC</title>
|
||||
<script type="module" crossorigin src="/_frontend/assets/index-DkP6_MI5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-BIG4WK1s.css">
|
||||
<script type="module" crossorigin src="/_frontend/assets/index-BHtumi6P.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-DP4QFhOv.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="page"></div>
|
||||
|
||||
Reference in New Issue
Block a user