Formular-Infrastruktur: Contact/Demo/Helpdesk mit E-Mail- und Salesforce-Delivery, JSON-Renderer-Erweiterungen, Crop-Variants
Build-Artefakte in public/_frontend/assets nicht enthalten.
22
packages/vitec/Classes/Controller/FormController.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
/**
|
||||
* FormController — Extbase registration target for the VITEC form plugins
|
||||
* (Contactform, Demoform, Helpdeskform). In headless mode the JSON output
|
||||
* is produced by FormsJsonRenderer; submissions are handled by
|
||||
* FormSubmissionMiddleware.
|
||||
*/
|
||||
class FormController extends ActionController
|
||||
{
|
||||
public function listAction(): ResponseInterface
|
||||
{
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\FormsJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
@@ -117,6 +118,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
||||
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
|
||||
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
|
||||
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
|
||||
'vitec_contactform' => [FormsJsonRenderer::class, 'form'],
|
||||
'vitec_demoform' => [FormsJsonRenderer::class, 'form'],
|
||||
'vitec_helpdeskform' => [FormsJsonRenderer::class, 'form'],
|
||||
'news_pi1' => [NewsJsonRenderer::class, 'news'],
|
||||
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
|
||||
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
|
||||
|
||||
19
packages/vitec/Classes/Forms/Delivery/DeliveryInterface.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
/**
|
||||
* Delivery strategy for VITEC form submissions.
|
||||
* Implementations MUST throw on failure — the middleware logs the error
|
||||
* on the stored submission and keeps the data safe.
|
||||
*/
|
||||
interface DeliveryInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string,string> $payload validated, filtered field values
|
||||
* @param array<string,mixed> $settings FlexForm settings of the form plugin
|
||||
*/
|
||||
public function deliver(string $formKey, array $payload, array $settings): void;
|
||||
}
|
||||
48
packages/vitec/Classes/Forms/Delivery/EmailDelivery.php
Executable file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
use TYPO3\CMS\Core\Mail\MailMessage;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Sends the submission as a plain-text mail to the FlexForm-configured
|
||||
* recipient(s). Sender = system default (MAIL settings); reply-to = the
|
||||
* submitter's email when present.
|
||||
*/
|
||||
final class EmailDelivery implements DeliveryInterface
|
||||
{
|
||||
public function deliver(string $formKey, array $payload, array $settings): void
|
||||
{
|
||||
$recipients = GeneralUtility::trimExplode(',', (string)($settings['recipient'] ?? ''), true);
|
||||
if ($recipients === []) {
|
||||
throw new \RuntimeException('No recipient configured in the form plugin (FlexForm "Recipient").');
|
||||
}
|
||||
|
||||
$subject = trim((string)($settings['subject'] ?? ''));
|
||||
if ($subject === '') {
|
||||
$subject = 'VITEC website form: ' . $formKey;
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
foreach ($payload as $field => $value) {
|
||||
$lines[] = str_pad($field . ':', 16) . $value;
|
||||
}
|
||||
|
||||
$mail = GeneralUtility::makeInstance(MailMessage::class);
|
||||
$mail->to(...$recipients)
|
||||
->subject($subject)
|
||||
->text("Form: $formKey\n\n" . implode("\n", $lines));
|
||||
|
||||
$submitterEmail = trim((string)($payload['email'] ?? ''));
|
||||
if ($submitterEmail !== '' && filter_var($submitterEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->replyTo($submitterEmail);
|
||||
}
|
||||
|
||||
if (!$mail->send()) {
|
||||
throw new \RuntimeException('MailMessage::send() reported failure.');
|
||||
}
|
||||
}
|
||||
}
|
||||
25
packages/vitec/Classes/Forms/Delivery/SalesforceDelivery.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
/**
|
||||
* Salesforce delivery — PREPARED STUB.
|
||||
*
|
||||
* The old website posted the contact form to Salesforce Web-to-Lead
|
||||
* (custom field ids like 00N3z00000DCiyV). When credentials / the target
|
||||
* setup are decided, implement deliver() here (Web-to-Lead POST or REST
|
||||
* API) including the camelCase -> Salesforce field mapping. Until then any
|
||||
* submission with delivery=salesforce is stored with status "failed" and
|
||||
* this message — no data is lost.
|
||||
*/
|
||||
final class SalesforceDelivery implements DeliveryInterface
|
||||
{
|
||||
public function deliver(string $formKey, array $payload, array $settings): void
|
||||
{
|
||||
throw new \RuntimeException(
|
||||
'Salesforce delivery is prepared but not configured yet — submission kept in the log.'
|
||||
);
|
||||
}
|
||||
}
|
||||
190
packages/vitec/Classes/Forms/FormDefinitions.php
Executable file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms;
|
||||
|
||||
/**
|
||||
* Single source of truth for the VITEC form plugins.
|
||||
*
|
||||
* The SAME definition drives (a) the JSON the React frontend renders the
|
||||
* form from and (b) the server-side validation of submissions in
|
||||
* FormSubmissionMiddleware. Field names are camelCase (React contract);
|
||||
* mapping to Salesforce field ids happens inside the Salesforce delivery.
|
||||
*
|
||||
* Field spec: name, type (text|email|tel|select|textarea), label, required,
|
||||
* options (select only) OR optionsSource (frontend-supplied list, e.g. the
|
||||
* ISO country list).
|
||||
*/
|
||||
final class FormDefinitions
|
||||
{
|
||||
public const HONEYPOT_FIELD = '_website';
|
||||
|
||||
/** CType => form key */
|
||||
public const CTYPE_MAP = [
|
||||
'vitec_contactform' => 'contact',
|
||||
'vitec_demoform' => 'demo',
|
||||
'vitec_helpdeskform' => 'helpdesk',
|
||||
];
|
||||
|
||||
private const SOLUTION_OPTIONS = [
|
||||
'IPTV Distribution',
|
||||
'Digital Signage',
|
||||
'Video Streaming',
|
||||
'Video Contribution',
|
||||
'Situational Awareness and ISR',
|
||||
'Remote Production',
|
||||
'Encoders & Decoders',
|
||||
'Custom Design Service',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function get(string $formKey): ?array
|
||||
{
|
||||
$all = self::all();
|
||||
return $all[$formKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$contactFields = [
|
||||
self::text('firstName', 'First Name', true),
|
||||
self::text('lastName', 'Last Name', true),
|
||||
self::text('company', 'Company', true),
|
||||
self::field('email', 'email', 'Email', true),
|
||||
self::field('phone', 'tel', 'Phone', true),
|
||||
self::text('street', 'Street', false),
|
||||
self::text('city', 'City', false),
|
||||
self::text('zip', 'Zip / Postal Code', false),
|
||||
self::countrySelect(),
|
||||
self::select('state', 'State', false, [], 'usStates'),
|
||||
self::select('solution', 'Solution of Interest', false, self::SOLUTION_OPTIONS),
|
||||
self::field('message', 'textarea', 'Message', false),
|
||||
];
|
||||
|
||||
return [
|
||||
'contact' => [
|
||||
'formKey' => 'contact',
|
||||
'title' => 'Contact VITEC',
|
||||
'fields' => $contactFields,
|
||||
],
|
||||
'demo' => [
|
||||
'formKey' => 'demo',
|
||||
'title' => 'Request a Demo',
|
||||
'fields' => $contactFields,
|
||||
],
|
||||
'helpdesk' => [
|
||||
'formKey' => 'helpdesk',
|
||||
'title' => 'Request Access to Online HelpDesk',
|
||||
'fields' => [
|
||||
self::text('firstName', 'First Name', true),
|
||||
self::text('lastName', 'Last Name', true),
|
||||
self::text('company', 'Company', true),
|
||||
self::field('phone', 'tel', 'Phone Number', false),
|
||||
self::field('email', 'email', 'Email Address', true),
|
||||
self::countrySelect(true),
|
||||
self::text('product', 'Product', false),
|
||||
self::text('serialNumber', 'Serial Number', false),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a submission payload against the definition.
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,string> field => error (empty = valid)
|
||||
*/
|
||||
public static function validate(string $formKey, array $payload): array
|
||||
{
|
||||
$definition = self::get($formKey);
|
||||
if ($definition === null) {
|
||||
return ['_form' => 'Unknown form'];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($definition['fields'] as $field) {
|
||||
$name = $field['name'];
|
||||
$value = trim((string)($payload[$name] ?? ''));
|
||||
|
||||
if (($field['required'] ?? false) && $value === '') {
|
||||
$errors[$name] = 'This field is required.';
|
||||
continue;
|
||||
}
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
if ($field['type'] === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$name] = 'Please enter a valid email address.';
|
||||
}
|
||||
if (mb_strlen($value) > 5000) {
|
||||
$errors[$name] = 'Value is too long.';
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce a raw payload to the defined fields (drops everything unknown).
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function filterPayload(string $formKey, array $payload): array
|
||||
{
|
||||
$definition = self::get($formKey);
|
||||
if ($definition === null) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($definition['fields'] as $field) {
|
||||
$out[$field['name']] = trim((string)($payload[$field['name']] ?? ''));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ field DSL
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function field(string $name, string $type, string $label, bool $required): array
|
||||
{
|
||||
return ['name' => $name, 'type' => $type, 'label' => $label, 'required' => $required];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function text(string $name, string $label, bool $required): array
|
||||
{
|
||||
return self::field($name, 'text', $label, $required);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function select(string $name, string $label, bool $required, array $options, string $optionsSource = ''): array
|
||||
{
|
||||
$field = self::field($name, 'select', $label, $required);
|
||||
if ($options !== []) {
|
||||
$field['options'] = $options;
|
||||
}
|
||||
if ($optionsSource !== '') {
|
||||
// The frontend supplies this list (e.g. ISO countries) — keeps
|
||||
// the definition lean and the lists consistent app-wide.
|
||||
$field['optionsSource'] = $optionsSource;
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function countrySelect(bool $required = false): array
|
||||
{
|
||||
return self::select('country', 'Country', $required, [], 'countries');
|
||||
}
|
||||
}
|
||||
170
packages/vitec/Classes/Middleware/FormSubmissionMiddleware.php
Executable file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Middleware;
|
||||
|
||||
use Evomedien\Vitec\Forms\Delivery\DeliveryInterface;
|
||||
use Evomedien\Vitec\Forms\Delivery\EmailDelivery;
|
||||
use Evomedien\Vitec\Forms\Delivery\SalesforceDelivery;
|
||||
use Evomedien\Vitec\Forms\FormDefinitions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Headless form endpoint: POST /api/vitec/form/<formKey>
|
||||
*
|
||||
* Accepts JSON (or form-encoded) submissions from the React frontend for
|
||||
* the VITEC form plugins (contact | demo | helpdesk):
|
||||
*
|
||||
* 1. honeypot check (silently accepted, nothing stored)
|
||||
* 2. server-side validation against FormDefinitions (single source of truth)
|
||||
* 3. submission stored in tx_vitec_form_submission (backup log)
|
||||
* 4. delivery via strategy: email (active) or salesforce (prepared stub) —
|
||||
* configured in the FlexForm of the form's plugin element
|
||||
*
|
||||
* A stored-but-undelivered submission still responds success:true — the
|
||||
* data is safe in the log and the delivery status is visible in the backend.
|
||||
*/
|
||||
final class FormSubmissionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private const PATH_PATTERN = '#^/api/vitec/form/([a-z]+)/?$#';
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$path = $request->getUri()->getPath();
|
||||
if (preg_match(self::PATH_PATTERN, $path, $m) !== 1) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
$formKey = $m[1];
|
||||
|
||||
if (strtoupper($request->getMethod()) !== 'POST') {
|
||||
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'POST only']], 405);
|
||||
}
|
||||
if (FormDefinitions::get($formKey) === null) {
|
||||
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unknown form']], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = $this->parseBody($request);
|
||||
|
||||
// Honeypot: pretend success, store nothing.
|
||||
if (trim((string)($payload[FormDefinitions::HONEYPOT_FIELD] ?? '')) !== '') {
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
$errors = FormDefinitions::validate($formKey, $payload);
|
||||
if ($errors !== []) {
|
||||
return new JsonResponse(['success' => false, 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
$clean = FormDefinitions::filterPayload($formKey, $payload);
|
||||
$settings = $this->pluginSettings($formKey);
|
||||
$method = (string)($settings['delivery'] ?? 'email');
|
||||
|
||||
$submissionUid = $this->store($formKey, $clean, $method);
|
||||
|
||||
try {
|
||||
$this->delivery($method)->deliver($formKey, $clean, $settings);
|
||||
$this->updateStatus($submissionUid, 'sent', '');
|
||||
} catch (\Throwable $e) {
|
||||
$this->updateStatus($submissionUid, 'failed', $e->getMessage());
|
||||
}
|
||||
|
||||
// Data is safely logged either way.
|
||||
return new JsonResponse(['success' => true]);
|
||||
} catch (\Throwable $e) {
|
||||
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unexpected error']], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function parseBody(ServerRequestInterface $request): array
|
||||
{
|
||||
$contentType = $request->getHeaderLine('Content-Type');
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
$decoded = json_decode((string)$request->getBody(), true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
$parsed = $request->getParsedBody();
|
||||
return is_array($parsed) ? $parsed : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery settings come from the FlexForm of the (first) plugin element
|
||||
* of this form type — one configuration per form type.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function pluginSettings(string $formKey): array
|
||||
{
|
||||
try {
|
||||
$ctype = array_search($formKey, FormDefinitions::CTYPE_MAP, true);
|
||||
if ($ctype === false) {
|
||||
return [];
|
||||
}
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||
$row = $qb
|
||||
->select('pi_flexform')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$qb->expr()->eq('CType', $qb->createNamedParameter($ctype)),
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->orderBy('uid', 'ASC')
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
if (!$row) {
|
||||
return [];
|
||||
}
|
||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||
$data = $flexFormService->convertFlexFormContentToArray((string)$row['pi_flexform']);
|
||||
return $data['settings'] ?? [];
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function delivery(string $method): DeliveryInterface
|
||||
{
|
||||
return $method === 'salesforce'
|
||||
? GeneralUtility::makeInstance(SalesforceDelivery::class)
|
||||
: GeneralUtility::makeInstance(EmailDelivery::class);
|
||||
}
|
||||
|
||||
/** @param array<string,string> $payload */
|
||||
private function store(string $formKey, array $payload, string $method): int
|
||||
{
|
||||
$conn = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('tx_vitec_form_submission');
|
||||
$conn->insert('tx_vitec_form_submission', [
|
||||
'pid' => 0,
|
||||
'crdate' => time(),
|
||||
'tstamp' => time(),
|
||||
'form_key' => $formKey,
|
||||
'payload' => (string)json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||
'delivery_method' => $method,
|
||||
'delivery_status' => 'pending',
|
||||
]);
|
||||
return (int)$conn->lastInsertId();
|
||||
}
|
||||
|
||||
private function updateStatus(int $uid, string $status, string $error): void
|
||||
{
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('tx_vitec_form_submission')
|
||||
->update(
|
||||
'tx_vitec_form_submission',
|
||||
['delivery_status' => $status, 'delivery_error' => mb_substr($error, 0, 1000), 'tstamp' => time()],
|
||||
['uid' => $uid]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\FormsJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
|
||||
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
@@ -82,6 +83,9 @@ final class ContentElementResolver
|
||||
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
|
||||
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
|
||||
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
|
||||
'vitec_contactform' => [FormsJsonRenderer::class, 'form'],
|
||||
'vitec_demoform' => [FormsJsonRenderer::class, 'form'],
|
||||
'vitec_helpdeskform' => [FormsJsonRenderer::class, 'form'],
|
||||
'news_pi1' => [NewsJsonRenderer::class, 'news'],
|
||||
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
|
||||
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
|
||||
|
||||
@@ -9,6 +9,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||
|
||||
/**
|
||||
* Serialises a Success Story (tx_vitec_domain_model_usecase) DB row to the
|
||||
@@ -43,7 +44,7 @@ final class UsecaseSerializer
|
||||
'slug' => (string)($u['slug'] ?? ''),
|
||||
'subtitle' => (string)($u['subtitle'] ?? ''),
|
||||
'teaser' => (string)($u['teaser'] ?? ''),
|
||||
'cardImage' => $this->image($uid, 'card_image'),
|
||||
'cardImage' => $this->image($uid, 'card_image', null, true),
|
||||
'customerLogo' => $this->image($uid, 'customer_logo'),
|
||||
'markets' => $this->relationMulti('tx_vitec_usecase_market_mm', 'tx_vitec_domain_model_market', $uid),
|
||||
'categories' => $this->categories($uid),
|
||||
@@ -100,7 +101,7 @@ final class UsecaseSerializer
|
||||
*
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public function image(int $recordUid, string $fieldName, ?string $table = null): ?array
|
||||
public function image(int $recordUid, string $fieldName, ?string $table = null, bool $withCropVariants = false): ?array
|
||||
{
|
||||
try {
|
||||
$table = $table ?? self::TABLE;
|
||||
@@ -142,43 +143,92 @@ final class UsecaseSerializer
|
||||
];
|
||||
}
|
||||
|
||||
$cropString = (string)($ref['crop'] ?? '');
|
||||
$payload = $this->processImagePayload($imageService, $fileReference, $cropString !== '' ? $cropString : null);
|
||||
|
||||
$srcset = [];
|
||||
foreach (self::IMAGE_WIDTHS as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
'width' => $width,
|
||||
'descriptor' => $width . 'w',
|
||||
];
|
||||
}
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $ref['crop'] ?? null]
|
||||
);
|
||||
|
||||
return [
|
||||
$result = [
|
||||
'uid' => (int)$ref['uid'],
|
||||
'url' => $imageService->getImageUri($default),
|
||||
'url' => $payload['url'],
|
||||
'title' => (string)($ref['title'] ?? ''),
|
||||
'alternative' => (string)($ref['alternative'] ?? ''),
|
||||
'description' => (string)($ref['description'] ?? ''),
|
||||
'srcset' => $srcset,
|
||||
'srcset' => $payload['srcset'],
|
||||
'properties' => [
|
||||
'width' => $fileReference->getProperty('width'),
|
||||
'height' => $fileReference->getProperty('height'),
|
||||
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||
],
|
||||
];
|
||||
|
||||
// Zusatz-Zuschnitte (Card 4:3 / Large Card 16:9) nur fuer das "erste Bild"
|
||||
// der karten-faehigen Modelle; faellt auf das Vollbild zurueck, wenn keine
|
||||
// Variante gesetzt ist.
|
||||
if ($withCropVariants) {
|
||||
$result['crops'] = $this->cropVariantPayloads($imageService, $fileReference, $cropString);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {url, srcset} for one crop input (crop JSON string, absolute Area
|
||||
* or null = full image). srcset steps whose processing would fall back to
|
||||
* the original file are skipped (avoids raw-/403-leaks for sources that are
|
||||
* smaller than the requested width).
|
||||
*
|
||||
* @param string|\TYPO3\CMS\Core\Imaging\ImageManipulation\Area|null $crop
|
||||
* @return array{url:string,srcset:array<int,array<string,mixed>>}
|
||||
*/
|
||||
private function processImagePayload(ImageService $imageService, \TYPO3\CMS\Core\Resource\FileReference $fileReference, $crop): array
|
||||
{
|
||||
$srcset = [];
|
||||
foreach (self::IMAGE_WIDTHS as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $crop, 'fileExtension' => 'webp']
|
||||
);
|
||||
if ($variant->usesOriginalFile()) {
|
||||
continue;
|
||||
}
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
'width' => $width,
|
||||
'descriptor' => $width . 'w',
|
||||
];
|
||||
}
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $crop, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
'url' => $imageService->getImageUri($default),
|
||||
'srcset' => $srcset,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* card + largeCard crop-variant payloads for a model's first image.
|
||||
* Each variant falls back to the full image when no explicit crop is set.
|
||||
*
|
||||
* @return array<string,array{url:string,srcset:array<int,array<string,mixed>>}>
|
||||
*/
|
||||
private function cropVariantPayloads(ImageService $imageService, \TYPO3\CMS\Core\Resource\FileReference $fileReference, string $cropString): array
|
||||
{
|
||||
$collection = CropVariantCollection::create($cropString);
|
||||
$out = [];
|
||||
foreach (['card', 'largeCard'] as $variantKey) {
|
||||
$area = $collection->getCropArea($variantKey);
|
||||
$crop = $area->isEmpty() ? null : $area->makeAbsoluteBasedOnFile($fileReference);
|
||||
$out[$variantKey] = $this->processImagePayload($imageService, $fileReference, $crop);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* All FAL images of one field (multi-reference), same shape as image().
|
||||
*
|
||||
@@ -260,13 +310,16 @@ final class UsecaseSerializer
|
||||
foreach (self::IMAGE_WIDTHS as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
if ($variant->usesOriginalFile()) {
|
||||
continue; // Quelle zu klein fuer diese Breite: kein Raw-/403-Leak
|
||||
}
|
||||
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
||||
}
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $ref['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
56
packages/vitec/Classes/Tca/CropVariants.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Tca;
|
||||
|
||||
/**
|
||||
* Zentrale cropVariants-Definition fuer das "erste Bild" der Karten-faehigen
|
||||
* Modelle (Product, Success Story, Market, Solution).
|
||||
*
|
||||
* Liefert drei Zuschnitt-Varianten, die im Backend-Image-Editor je einen
|
||||
* eigenen Tab bekommen:
|
||||
* - default : freier/allgemeiner Zuschnitt (Detail-/Hero-/Listen-Nutzung)
|
||||
* - card : 4:3 fuer die normale Card-Darstellung
|
||||
* - largeCard : 16:9 fuer die grosse/Feature-Card-Darstellung
|
||||
*
|
||||
* Ein freier Zuschnitt ("Free") ist bei jeder Variante zusaetzlich moeglich;
|
||||
* die genannte Ratio ist lediglich der vorgegebene Standard-Rahmen.
|
||||
*
|
||||
* Wird aus den TCA-Dateien der o.g. Modelle heraus referenziert:
|
||||
* 'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||
*/
|
||||
final class CropVariants
|
||||
{
|
||||
/**
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
public static function firstImage(): array
|
||||
{
|
||||
return [
|
||||
'default' => [
|
||||
'title' => 'Default',
|
||||
'allowedAspectRatios' => [
|
||||
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||
'16:9' => ['title' => '16:9', 'value' => 16 / 9],
|
||||
'4:3' => ['title' => '4:3', 'value' => 4 / 3],
|
||||
'1:1' => ['title' => '1:1', 'value' => 1.0],
|
||||
],
|
||||
],
|
||||
'card' => [
|
||||
'title' => 'Card (4:3)',
|
||||
'allowedAspectRatios' => [
|
||||
'4:3' => ['title' => '4:3', 'value' => 4 / 3],
|
||||
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||
],
|
||||
],
|
||||
'largeCard' => [
|
||||
'title' => 'Large Card (16:9)',
|
||||
'allowedAspectRatios' => [
|
||||
'16:9' => ['title' => '16:9', 'value' => 16 / 9],
|
||||
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -204,13 +204,13 @@ class CustomerlogosJsonRenderer
|
||||
foreach (self::IMAGE_WIDTHS as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
||||
}
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 400, 'crop' => $ref['crop'] ?? null]
|
||||
['width' => 400, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -338,7 +338,7 @@ class DatasheetsJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -211,7 +211,7 @@ class DownloadcardcollectionJsonRenderer
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -222,7 +222,7 @@ class DownloadcardcollectionJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -247,7 +247,7 @@ class EventlistJsonRenderer
|
||||
foreach ([400, 800] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -258,7 +258,7 @@ class EventlistJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
47
packages/vitec/Classes/UserFunc/FaviconsJsonRenderer.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
|
||||
/**
|
||||
* Headless JSON renderer for the site-wide favicon / app-icon set.
|
||||
*
|
||||
* Emits a stable `favicons` object into every page response so the React
|
||||
* frontend can render the <link>/<meta> head tags from the CMS instead of
|
||||
* hard-coding them. The icon files live under public/fileadmin/icons/ and are
|
||||
* referenced root-relative — identical convention to the image URLs emitted by
|
||||
* UsecaseSerializer::image() (getImageUri also returns /fileadmin/... paths).
|
||||
* If the frontend ever runs on a separate origin from the CMS, it must prefix
|
||||
* these hrefs with the CMS base URL, exactly as it does for image URLs.
|
||||
*
|
||||
* Static by nature (same for every page) — no DB access, exception-free.
|
||||
*/
|
||||
final class FaviconsJsonRenderer
|
||||
{
|
||||
private const BASE = '/fileadmin/icons';
|
||||
|
||||
/** Browser UI / address-bar tint (VITEC navy). */
|
||||
private const THEME_COLOR = '#26358C';
|
||||
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
$payload = [
|
||||
'themeColor' => self::THEME_COLOR,
|
||||
'manifest' => self::BASE . '/site.webmanifest',
|
||||
// Ready-to-render <link> descriptors, ordered most- to least-specific.
|
||||
'links' => [
|
||||
['rel' => 'icon', 'type' => 'image/x-icon', 'href' => self::BASE . '/favicon.ico'],
|
||||
['rel' => 'icon', 'type' => 'image/png', 'sizes' => '32x32', 'href' => self::BASE . '/favicon-32.png'],
|
||||
['rel' => 'icon', 'type' => 'image/png', 'sizes' => '16x16', 'href' => self::BASE . '/favicon-16.png'],
|
||||
['rel' => 'apple-touch-icon', 'sizes' => '180x180', 'href' => self::BASE . '/apple-touch-icon.png'],
|
||||
['rel' => 'manifest', 'href' => self::BASE . '/site.webmanifest'],
|
||||
],
|
||||
];
|
||||
|
||||
return json_encode($payload) ?: '';
|
||||
}
|
||||
}
|
||||
108
packages/vitec/Classes/UserFunc/FormsJsonRenderer.php
Executable file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Forms\FormDefinitions;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* UserFunc: render ONE of the VITEC form plugins as JSON (headless).
|
||||
* Serves vitec_contactform, vitec_demoform and vitec_helpdeskform — the
|
||||
* CType picks the definition from FormDefinitions.
|
||||
*
|
||||
* Output under content.form:
|
||||
* { "formKey": "contact", "title": "…", "endpoint": "/api/vitec/form/contact",
|
||||
* "honeypot": "_website", "fields": [ … ] }
|
||||
*
|
||||
* The React frontend renders the fields generically and POSTs JSON to the
|
||||
* endpoint (handled by FormSubmissionMiddleware). Exception-safe.
|
||||
*/
|
||||
class FormsJsonRenderer
|
||||
{
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
|
||||
if ($row && isset(FormDefinitions::CTYPE_MAP[(string)($row['CType'] ?? '')])) {
|
||||
return $this->renderForRecord($row);
|
||||
}
|
||||
|
||||
$pageId = 0;
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
if ($request !== null) {
|
||||
$pageInfo = $request->getAttribute('frontend.page.information');
|
||||
if ($pageInfo !== null) {
|
||||
$pageId = (int)$pageInfo->getId();
|
||||
}
|
||||
}
|
||||
if ($pageId <= 0) {
|
||||
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||
}
|
||||
if ($pageId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||
$ces = $qb
|
||||
->select('*')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||
$qb->expr()->in('CType', $qb->createNamedParameter(
|
||||
array_keys(FormDefinitions::CTYPE_MAP),
|
||||
\TYPO3\CMS\Core\Database\Connection::PARAM_STR_ARRAY
|
||||
)),
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
if (empty($ces)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->renderForRecord($ces[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $contentElement
|
||||
*/
|
||||
public function renderForRecord(array $contentElement): string
|
||||
{
|
||||
try {
|
||||
$formKey = FormDefinitions::CTYPE_MAP[(string)($contentElement['CType'] ?? '')] ?? '';
|
||||
$definition = FormDefinitions::get($formKey);
|
||||
if ($definition === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||
$settings = $flexFormData['settings'] ?? [];
|
||||
$debugMode = (bool)($settings['debug'] ?? false);
|
||||
|
||||
$response = [
|
||||
'formKey' => $definition['formKey'],
|
||||
'title' => $definition['title'],
|
||||
'endpoint' => '/api/vitec/form/' . $definition['formKey'],
|
||||
'honeypot' => FormDefinitions::HONEYPOT_FIELD,
|
||||
'fields' => $definition['fields'],
|
||||
];
|
||||
|
||||
if ($debugMode) {
|
||||
$response['debug'] = ['settings' => $settings];
|
||||
}
|
||||
|
||||
return (string)json_encode($response);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class MarketShowJsonRenderer
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -212,7 +212,7 @@ class MarketShowJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -157,8 +157,8 @@ class ModelcardJsonRenderer
|
||||
'slug' => (string)($row['slug'] ?? ''),
|
||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||
'teaser' => (string)($row['teaser'] ?? ''),
|
||||
'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product')
|
||||
?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product'),
|
||||
'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product', true)
|
||||
?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product', true),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class ModelcardJsonRenderer
|
||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||
'teaser' => (string)($row['teaser'] ?? ''),
|
||||
'description' => (string)($row['description'] ?? ''),
|
||||
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType]),
|
||||
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType], true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@ final class NewsJsonRenderer
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
$fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']);
|
||||
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400]);
|
||||
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400, 'fileExtension' => 'webp']);
|
||||
|
||||
$files[] = [
|
||||
'uid' => (int)$row['uid'],
|
||||
|
||||
@@ -246,7 +246,8 @@ class ProductListJsonRenderer
|
||||
[
|
||||
'width' => $dimensions['width'],
|
||||
'height' => $dimensions['height'],
|
||||
'crop' => $fileRefData['crop'] ?? null
|
||||
'crop' => $fileRefData['crop'] ?? null,
|
||||
'fileExtension' => 'webp'
|
||||
]
|
||||
);
|
||||
|
||||
@@ -260,7 +261,7 @@ class ProductListJsonRenderer
|
||||
|
||||
$defaultProcessed = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
$images[] = [
|
||||
@@ -318,7 +319,7 @@ class ProductListJsonRenderer
|
||||
|
||||
$processed = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -327,14 +327,14 @@ class ProductShowJsonRenderer
|
||||
|
||||
$processedImage = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => '1874c', 'height' => '625c']
|
||||
['width' => '1874c', 'height' => '625c', 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
$srcset = [];
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$processedVariant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c']
|
||||
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c', 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($processedVariant),
|
||||
@@ -399,7 +399,7 @@ class ProductShowJsonRenderer
|
||||
|
||||
$processedImage = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 1200]
|
||||
['width' => 1200, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -201,7 +201,7 @@ class SolutionShowJsonRenderer
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -212,7 +212,7 @@ class SolutionShowJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
65
packages/vitec/Configuration/FlexForms/Contactform.xml
Executable file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>Contact Form Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.delivery>
|
||||
<label>Delivery</label>
|
||||
<onChange>reload</onChange>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>E-Mail</label>
|
||||
<value>email</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Salesforce (prepared — not active yet)</label>
|
||||
<value>salesforce</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>email</default>
|
||||
</config>
|
||||
</settings.delivery>
|
||||
|
||||
<settings.recipient>
|
||||
<label>Recipient(s)</label>
|
||||
<description>Comma-separated email addresses.</description>
|
||||
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||
<config>
|
||||
<type>input</type>
|
||||
<size>50</size>
|
||||
<eval>trim</eval>
|
||||
</config>
|
||||
</settings.recipient>
|
||||
|
||||
<settings.subject>
|
||||
<label>Mail Subject</label>
|
||||
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||
<config>
|
||||
<type>input</type>
|
||||
<size>50</size>
|
||||
<eval>trim</eval>
|
||||
<default>VITEC website: contact request</default>
|
||||
</config>
|
||||
</settings.subject>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output</label>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.debug>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
65
packages/vitec/Configuration/FlexForms/Demoform.xml
Executable file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>Request Demo Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.delivery>
|
||||
<label>Delivery</label>
|
||||
<onChange>reload</onChange>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>E-Mail</label>
|
||||
<value>email</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Salesforce (prepared — not active yet)</label>
|
||||
<value>salesforce</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>email</default>
|
||||
</config>
|
||||
</settings.delivery>
|
||||
|
||||
<settings.recipient>
|
||||
<label>Recipient(s)</label>
|
||||
<description>Comma-separated email addresses.</description>
|
||||
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||
<config>
|
||||
<type>input</type>
|
||||
<size>50</size>
|
||||
<eval>trim</eval>
|
||||
</config>
|
||||
</settings.recipient>
|
||||
|
||||
<settings.subject>
|
||||
<label>Mail Subject</label>
|
||||
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||
<config>
|
||||
<type>input</type>
|
||||
<size>50</size>
|
||||
<eval>trim</eval>
|
||||
<default>VITEC website: demo request</default>
|
||||
</config>
|
||||
</settings.subject>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output</label>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.debug>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
65
packages/vitec/Configuration/FlexForms/Helpdeskform.xml
Executable file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<T3DataStructure>
|
||||
<meta>
|
||||
<langDisable>1</langDisable>
|
||||
</meta>
|
||||
<sheets>
|
||||
<sDEF>
|
||||
<ROOT>
|
||||
<sheetTitle>HelpDesk Access Settings</sheetTitle>
|
||||
<type>array</type>
|
||||
<el>
|
||||
<settings.delivery>
|
||||
<label>Delivery</label>
|
||||
<onChange>reload</onChange>
|
||||
<config>
|
||||
<type>select</type>
|
||||
<renderType>selectSingle</renderType>
|
||||
<items>
|
||||
<numIndex index="0">
|
||||
<label>E-Mail</label>
|
||||
<value>email</value>
|
||||
</numIndex>
|
||||
<numIndex index="1">
|
||||
<label>Salesforce (prepared — not active yet)</label>
|
||||
<value>salesforce</value>
|
||||
</numIndex>
|
||||
</items>
|
||||
<default>email</default>
|
||||
</config>
|
||||
</settings.delivery>
|
||||
|
||||
<settings.recipient>
|
||||
<label>Recipient(s)</label>
|
||||
<description>Comma-separated email addresses.</description>
|
||||
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||
<config>
|
||||
<type>input</type>
|
||||
<size>50</size>
|
||||
<eval>trim</eval>
|
||||
</config>
|
||||
</settings.recipient>
|
||||
|
||||
<settings.subject>
|
||||
<label>Mail Subject</label>
|
||||
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||
<config>
|
||||
<type>input</type>
|
||||
<size>50</size>
|
||||
<eval>trim</eval>
|
||||
<default>VITEC website: helpdesk access request</default>
|
||||
</config>
|
||||
</settings.subject>
|
||||
|
||||
<settings.debug>
|
||||
<label>Allow Debug Output</label>
|
||||
<config>
|
||||
<type>check</type>
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.debug>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
</sheets>
|
||||
</T3DataStructure>
|
||||
@@ -88,4 +88,16 @@ return [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg',
|
||||
],
|
||||
'vitec-plugin-contactform' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-contactform.svg',
|
||||
],
|
||||
'vitec-plugin-demoform' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-demoform.svg',
|
||||
],
|
||||
'vitec-plugin-helpdeskform' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-helpdeskform.svg',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -7,6 +7,15 @@ declare(strict_types=1);
|
||||
*/
|
||||
return [
|
||||
'frontend' => [
|
||||
'vitec/form-submission' => [
|
||||
'target' => \Evomedien\Vitec\Middleware\FormSubmissionMiddleware::class,
|
||||
'after' => [
|
||||
'typo3/cms-core/normalized-params-attribute',
|
||||
],
|
||||
'before' => [
|
||||
'typo3/cms-frontend/site',
|
||||
],
|
||||
],
|
||||
'vitec/success-story-path-rewrite' => [
|
||||
'target' => \Evomedien\Vitec\Middleware\SuccessStoryPathRewrite::class,
|
||||
'after' => [
|
||||
|
||||
@@ -161,6 +161,21 @@ tt_content {
|
||||
}
|
||||
}
|
||||
|
||||
vitec_contactform < lib.contentElementWithHeader
|
||||
vitec_contactform {
|
||||
fields {
|
||||
content {
|
||||
fields {
|
||||
form = USER
|
||||
form.userFunc = Evomedien\Vitec\UserFunc\FormsJsonRenderer->render
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vitec_demoform < tt_content.vitec_contactform
|
||||
vitec_helpdeskform < tt_content.vitec_contactform
|
||||
|
||||
vitec_modelcard < lib.contentElementWithHeader
|
||||
vitec_modelcard {
|
||||
fields {
|
||||
|
||||
@@ -38,6 +38,37 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||
// Locations: lives in the "VITEC" wizard group, titled "VITEC Locations".
|
||||
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['vitec']
|
||||
= 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header';
|
||||
// VITEC Forms — own wizard tab for the three form plugins.
|
||||
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['vitec_forms']
|
||||
= 'VITEC Forms';
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Contactform',
|
||||
'VITEC Contact Form',
|
||||
'vitec-plugin-contactform',
|
||||
'vitec_forms',
|
||||
'Contact form — email or Salesforce delivery.',
|
||||
'FILE:EXT:vitec/Configuration/FlexForms/Contactform.xml'
|
||||
);
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Demoform',
|
||||
'VITEC Request Demo',
|
||||
'vitec-plugin-demoform',
|
||||
'vitec_forms',
|
||||
'Demo request form — email or Salesforce delivery.',
|
||||
'FILE:EXT:vitec/Configuration/FlexForms/Demoform.xml'
|
||||
);
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Helpdeskform',
|
||||
'VITEC HelpDesk Access',
|
||||
'vitec-plugin-helpdeskform',
|
||||
'vitec_forms',
|
||||
'Request access to the online helpdesk — email or Salesforce delivery.',
|
||||
'FILE:EXT:vitec/Configuration/FlexForms/Helpdeskform.xml'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'Vitec',
|
||||
'Modelcard',
|
||||
|
||||
@@ -164,6 +164,11 @@ return [
|
||||
'foreign_selector' => 'uid_local',
|
||||
'overrideChildTca' => [
|
||||
'columns' => [
|
||||
'crop' => [
|
||||
'config' => [
|
||||
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||
],
|
||||
],
|
||||
'uid_local' => [
|
||||
'config' => [
|
||||
'appearance' => [
|
||||
|
||||
@@ -443,6 +443,15 @@ return [
|
||||
'foreign_match_fields' => [
|
||||
'fieldname' => 'productimage',
|
||||
],
|
||||
'overrideChildTca' => [
|
||||
'columns' => [
|
||||
'crop' => [
|
||||
'config' => [
|
||||
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'appearance' => [
|
||||
'collapseAll' => true,
|
||||
'levelLinksPosition' => 'top',
|
||||
|
||||
@@ -164,6 +164,11 @@ return [
|
||||
'foreign_selector' => 'uid_local',
|
||||
'overrideChildTca' => [
|
||||
'columns' => [
|
||||
'crop' => [
|
||||
'config' => [
|
||||
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||
],
|
||||
],
|
||||
'uid_local' => [
|
||||
'config' => [
|
||||
'appearance' => [
|
||||
|
||||
@@ -117,7 +117,21 @@ return [
|
||||
// --------------------------------------------------------- List view
|
||||
'card_image' => [
|
||||
'label' => 'Card Image',
|
||||
'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'],
|
||||
'config' => [
|
||||
'type' => 'file',
|
||||
'maxitems' => 1,
|
||||
'allowed' => 'common-image-types',
|
||||
// Zusatz-Zuschnitte fuer Card-/Large-Card-Darstellung (freier Crop bleibt moeglich).
|
||||
'overrideChildTca' => [
|
||||
'columns' => [
|
||||
'crop' => [
|
||||
'config' => [
|
||||
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'customer_logo' => [
|
||||
'label' => 'Customer Logo',
|
||||
|
||||
41
packages/vitec/Configuration/TCA/tx_vitec_form_submission.php
Executable file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
return [
|
||||
'ctrl' => [
|
||||
'title' => 'VITEC Form Submission',
|
||||
'label' => 'form_key',
|
||||
'label_alt' => 'delivery_status',
|
||||
'label_alt_force' => true,
|
||||
'tstamp' => 'tstamp',
|
||||
'crdate' => 'crdate',
|
||||
'default_sortby' => 'crdate DESC',
|
||||
'rootLevel' => 1,
|
||||
'readOnly' => true,
|
||||
'iconfile' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-contactform.svg',
|
||||
'security' => ['ignorePageTypeRestriction' => true],
|
||||
],
|
||||
'types' => [
|
||||
'1' => ['showitem' => 'form_key, delivery_method, delivery_status, delivery_error, payload'],
|
||||
],
|
||||
'columns' => [
|
||||
'form_key' => [
|
||||
'label' => 'Form',
|
||||
'config' => ['type' => 'input', 'readOnly' => true],
|
||||
],
|
||||
'payload' => [
|
||||
'label' => 'Data (JSON)',
|
||||
'config' => ['type' => 'text', 'rows' => 12, 'readOnly' => true],
|
||||
],
|
||||
'delivery_method' => [
|
||||
'label' => 'Delivery',
|
||||
'config' => ['type' => 'input', 'readOnly' => true],
|
||||
],
|
||||
'delivery_status' => [
|
||||
'label' => 'Status',
|
||||
'config' => ['type' => 'input', 'readOnly' => true],
|
||||
],
|
||||
'delivery_error' => [
|
||||
'label' => 'Delivery Error',
|
||||
'config' => ['type' => 'text', 'rows' => 3, 'readOnly' => true],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -72,3 +72,10 @@ lib.metaMenu {
|
||||
page.10.fields.mainNavigation =< lib.mainNavigation
|
||||
page.10.fields.footerMenu =< lib.footerMenu
|
||||
page.10.fields.metaMenu =< lib.metaMenu
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Favicon / app-icon set (site-wide) — see FaviconsJsonRenderer
|
||||
# Icon files: public/fileadmin/icons/
|
||||
# -----------------------------------------------------------------------------
|
||||
page.10.fields.favicons = USER
|
||||
page.10.fields.favicons.userFunc = Evomedien\Vitec\UserFunc\FaviconsJsonRenderer->render
|
||||
|
||||
@@ -20,3 +20,15 @@
|
||||
.t3-page-ce.vitec-ce-collapsed .vitec-collapse-toggle typo3-backend-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Collapsed: the element's own preview header lives in the CE header row —
|
||||
full width on its own line below the icon/button row. */
|
||||
.t3-page-ce-header:has(.vitec-preview-header--in-header) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.element-preview-header.vitec-preview-header--in-header {
|
||||
flex-basis: 100%;
|
||||
width: 100%;
|
||||
order: 10;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect x="3" y="7" width="26" height="18" rx="2" fill="#0a3d62"/>
|
||||
<path d="M4 9l12 9 12-9" stroke="#ff6a00" stroke-width="2.5" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 235 B |
0
packages/vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 367 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect x="3" y="6" width="26" height="17" rx="2" fill="#0a3d62"/>
|
||||
<path d="M13 11l8 4.5-8 4.5z" fill="#ff6a00"/>
|
||||
<rect x="10" y="25" width="12" height="2.5" rx="1" fill="#9aa2ab"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<path d="M16 4a10 10 0 0 0-10 10v6a3 3 0 0 0 3 3h2v-8H8v-1a8 8 0 1 1 16 0v1h-3v8h2a3 3 0 0 0 3-3v-6A10 10 0 0 0 16 4z" fill="#0a3d62"/>
|
||||
<path d="M21 23v1.5a2.5 2.5 0 0 1-2.5 2.5H15" stroke="#ff6a00" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
0
packages/vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 226 B |
0
packages/vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
BIN
packages/vitec/Resources/Public/Images/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/vitec/Resources/Public/Images/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -118,14 +118,70 @@ class VitecContainerCollapse {
|
||||
if (this.isCollapsed(uid)) {
|
||||
ce.classList.add('vitec-ce-collapsed');
|
||||
}
|
||||
this.syncPreviewHeader(ce);
|
||||
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const collapsed = ce.classList.toggle('vitec-ce-collapsed');
|
||||
this.setCollapsed(uid, collapsed);
|
||||
this.syncPreviewHeader(ce);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the element's own `.element-preview-header` visible while collapsed:
|
||||
* it is moved into the CE header row (after `.t3-page-ce-header-right`) and
|
||||
* moved back to its original spot (placeholder marker) on expand.
|
||||
*/
|
||||
syncPreviewHeader(ce) {
|
||||
const element = ce.querySelector(':scope > .t3-page-ce-element');
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const header = element.querySelector(':scope > .t3-page-ce-header');
|
||||
const body = element.querySelector(':scope > .t3-page-ce-body');
|
||||
if (!header || !body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collapsed = ce.classList.contains('vitec-ce-collapsed');
|
||||
|
||||
if (collapsed) {
|
||||
// Find THIS element's preview header (not one of a nested child CE).
|
||||
let preview = null;
|
||||
for (const candidate of body.querySelectorAll('.element-preview-header')) {
|
||||
if (candidate.closest('.t3-page-ce') === ce) {
|
||||
preview = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!preview || header.contains(preview)) {
|
||||
return;
|
||||
}
|
||||
const placeholder = document.createElement('span');
|
||||
placeholder.className = 'vitec-preview-header-placeholder';
|
||||
placeholder.hidden = true;
|
||||
preview.before(placeholder);
|
||||
preview.classList.add('vitec-preview-header--in-header');
|
||||
const headerRight = header.querySelector('.t3-page-ce-header-right');
|
||||
if (headerRight) {
|
||||
headerRight.insertAdjacentElement('afterend', preview);
|
||||
} else {
|
||||
header.appendChild(preview);
|
||||
}
|
||||
} else {
|
||||
const preview = header.querySelector('.element-preview-header.vitec-preview-header--in-header');
|
||||
const placeholder = body.querySelector('.vitec-preview-header-placeholder');
|
||||
if (preview && placeholder) {
|
||||
preview.classList.remove('vitec-preview-header--in-header');
|
||||
placeholder.replaceWith(preview);
|
||||
} else if (preview) {
|
||||
preview.classList.remove('vitec-preview-header--in-header');
|
||||
body.prepend(preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new VitecContainerCollapse();
|
||||
|
||||
@@ -193,4 +193,38 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1750000000] = [
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'Vitec',
|
||||
'Contactform',
|
||||
[
|
||||
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||
],
|
||||
[
|
||||
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'Vitec',
|
||||
'Demoform',
|
||||
[
|
||||
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||
],
|
||||
[
|
||||
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'Vitec',
|
||||
'Helpdeskform',
|
||||
[
|
||||
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||
],
|
||||
[
|
||||
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||
]
|
||||
);
|
||||
|
||||
})();
|
||||
|
||||
@@ -281,3 +281,11 @@ CREATE TABLE tx_vitec_domain_model_customer (
|
||||
logo int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
emphasize_logo smallint(5) unsigned DEFAULT '0' NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tx_vitec_form_submission (
|
||||
form_key varchar(30) DEFAULT '' NOT NULL,
|
||||
payload text,
|
||||
delivery_method varchar(20) DEFAULT '' NOT NULL,
|
||||
delivery_status varchar(20) DEFAULT '' NOT NULL,
|
||||
delivery_error text
|
||||
);
|
||||
|
||||